【问题标题】:Python: Why doesn't this recursion mutate the input list?Python:为什么这个递归不会改变输入列表?
【发布时间】:2020-08-03 12:29:26
【问题描述】:

这是一个理解 Python 列表变异的问题。我解决了这个深度优先搜索问题,我得到了正确的结果。然后我得到了一些反馈,我认为这不应该有效,但它有效。

基本上,如果我使用 path[0] 而不是 current_path (通过切换注释掉和非注释行对中的行),结果是相同的。我试图理解为什么会这样。我了解列表路径中的列表,即路径 [0],没有发生突变。但是,当我将不同的列表分配给路径的第 0 个索引时,列表路径应该会发生变化。然而,它工作得很好。

以下是代码,我留下了文档字符串以进行进一步说明。最后,我粘贴了我玩过的代码以更好地理解列表突变,但这对我没有帮助。有人可以解释一下吗?

# Problem 3b: Implement get_best_path
def get_best_path(digraph, start, end, path, max_dist_outdoors, best_dist,
                  best_path):
    """
    Finds the shortest path between buildings subject to constraints.
        digraph: Digraph instance
            The graph on which to carry out the search
        start: string, Building number at which to start
        end: string, Building number at which to end
        path: list composed of [[list of strings], int, int]
            Represents the current path of nodes being traversed. Contains
            a list of node names, total distance traveled, and total
            distance outdoors.
        max_dist_outdoors: int
            Maximum distance spent outdoors on a path
        best_dist: int
            The smallest distance between the original start and end node
            for the initial problem that you are trying to solve
        best_path: list of strings
            The shortest path found so far between the original start
            and end node.
    Returns:
        A tuple with the shortest-path from start to end, represented by
        a list of building numbers (in strings), [n_1, n_2, ..., n_k],
        where there exists an edge from n_i to n_(i+1) in digraph,
        for all 1 <= i < k and the distance of that path.

        If there exists no path that satisfies max_total_dist and
        max_dist_outdoors constraints, then return None.
    """
    if not digraph.has_node(Node(start)) or not digraph.has_node(Node(end)):
        raise ValueError('Start or end node, or both not in graph')

    current_path = path[0] + [start]
    path[0]="This line doesn't change a thing, if I use 'current_path', but why?!"
#    path[0] = path[0] + [start]

    if start == end:
        return [current_path, path[1], path[2]]
#        return path

    edges = digraph.get_edges_for_node(Node(start))
    for edge in edges:
        next_node = str(edge.get_destination())

        if next_node not in current_path: #avoiding cycles
#        if next_node not in path[0]: #avoiding cycles

            new_tot = path[1] + edge.get_total_distance()
            new_out = path[2] + edge.get_outdoor_distance()
            if (best_dist==None or best_dist>=new_tot) and new_out<=max_dist_outdoors:

                new_path=get_best_path(digraph, next_node, end, [current_path,new_tot,new_out], \
#                new_path=get_best_path(digraph, next_node, end, [path[0],new_tot,new_out], \
                                                    max_dist_outdoors, best_dist, best_path)

                if new_path != None:
                    best_path = new_path[0]
                    best_dist = new_path[1]
    if best_path == None:
        return None
    return [best_path, best_dist]

我在这里尝试了这个想法,但正如我所料,输入列表发生了变异。那么,为什么大写的“路径”列表没有发生变异?

def foo_mutating(bar):
    bar[0] = bar[0] + [1]
    print('bar inside foo:', bar)
    if len(bar[0]) == 5:
        return bar
    return foo_mutating(bar)

def foo_nonmut(bar):
    temp_bar0 = bar[0]+[1]
    print('bar inside foo:', bar)
    if len(temp_bar0) == 5:
        return [temp_bar0, bar[1], bar[2]]
    return foo_nonmut([temp_bar0, bar[1], bar[2]])

def test():
    print('-----TESTING NON-MUTATING----')
    bar_init = [[], 22, 33]
    print('bar_init:', bar_init)
    new_bar = foo_nonmut(bar_init)
    print('new_bar:', new_bar)
    print('bar_init:', bar_init)
    print('\n')
    print('-----TESTING MUTATING----')
    bar_init = [[], 22, 33]
    print('bar_init:', bar_init)
    new_bar = foo_mutating(bar_init)
    print('new_bar:', new_bar)
    print('bar_init:', bar_init)

test()

打印:

-----TESTING NON-MUTATING----
bar_init: [[], 22, 33]
bar inside foo: [[], 22, 33]
bar inside foo: [[1], 22, 33]
bar inside foo: [[1, 1], 22, 33]
bar inside foo: [[1, 1, 1], 22, 33]
bar inside foo: [[1, 1, 1, 1], 22, 33]
new_bar: [[1, 1, 1, 1, 1], 22, 33]
bar_init: [[], 22, 33]

-----TESTING MUTATING----
bar_init: [[], 22, 33]
bar inside foo: [[1], 22, 33]
bar inside foo: [[1, 1], 22, 33]
bar inside foo: [[1, 1, 1], 22, 33]
bar inside foo: [[1, 1, 1, 1], 22, 33]
bar inside foo: [[1, 1, 1, 1, 1], 22, 33]
new_bar: [[1, 1, 1, 1, 1], 22, 33]
bar_init: [[1, 1, 1, 1, 1], 22, 33]

【问题讨论】:

  • 你有没有机会提供一个更“最小”的minimal reproducible example?我确信有一个更短的代码可以重现您注意到的行为。
  • @DeepSpace ,我只花了 30 分钟试图减少问题,但我最终得到了其他东西,我认为我无法重现这种情况。我现在才学习编程 2-3 个月,所以语法、运算符和对象的更深层次的含义仍然有点模糊,所以我很难重现我不完全理解的东西。但对于未来的任何问题,我一定会牢记这一点。感谢您对问题的反馈!

标签: python list recursion mutation


【解决方案1】:

为什么这个递归不会改变输入列表?

因为它不是用输入列表调用的,因此它不能用于变异。

当你进行递归调用时(我重新格式化了一下):

# "non-mutating" approach, as in the original code
new_path = get_best_path(
    digraph, next_node, end, [current_path,new_tot,new_out],
    max_dist_outdoors, best_dist, best_path
)

# "mutating" approach, using the commented-out line
new_path=get_best_path(
    digraph, next_node, end, [path[0],new_tot,new_out],
    max_dist_outdoors, best_dist, best_path
)

算法尝试改变路径,创建一个传递给递归调用的新列表,因此它不会改变。与玩具示例对比:

# "non-mutating" recursive call
return foo_nonmut([temp_bar0, bar[1], bar[2]])

# "mutating" recursive call
return foo_mutating(bar)
# Notice, it does not make a new list.

一般来说,当您不依赖突变时,递归算法更容易推理。您可能还会发现通过使用某种类来表示路径,您可以获得更清晰、更易于理解的代码。然后,您可以为该类提供非变异方法,用于“创建一个带有附加到节点名称列表的指定节点的新路径”和“使用修改的距离数据创建一个新路径”。 (或者更好的是——同时给它一个 Edge 实例,从中获取所有必要的数据)。

类似:

class Path:
    def __init__(self, names, distance, outdoor):
        self._names, self._distance, self._outdoor = names, distance, outdoor

    def __contains__(self, node):
        return node in self._names

    def with_edge(self, edge):
        return Path(
            # incidentally, you should be using `property`s instead of methods
            # for this Edge data.
            self._names + [edge.get_destination()],
            self._distance + edge.get_total_distance(),
            self._outdoor + edge.get_outdoor_distance()
        )

    def better_than(self, other, outdoor_limit):
        if self._outdoor <= outdoor_limit:
            return False
        best_distance = other._distance
        return best_distance is None or best_distance >= self._distance

    def end(self): # where we start recursing from during pathfinding.
        return self._names[-1]

现在我们可以做一些类似的事情:

# instead of remembering best_path and best_dist separately, we'll just
# use a Path object for best_path, and we can ignore the outdoor distance
# when interpreting the results outside the recursion.
edges = digraph.get_edges_for_node(Node(start))
for edge in edges:
    if edge.get_destination() in path:
        continue
    candidate = path.with_edge(edge)
    if candidate.better_than(best_path, max_dist_outdoors):
        # We can remove some parameters from the recursive call,
        # because we can infer them from the passed-in `path`.
        new_path = get_best_path(digraph, end, path, max_dist_outdoors, best_path)
        if new_path is not None:
            best_path = new_path

【讨论】:

  • 感谢您的回答和解释!我将实现这样一个类,似乎是一个好主意和实践!但是现在我发现我没有问对正确的问题,所以我只得到了一半的图片......这是另一半:我们有一个 2 分支树,从 1 开始:1-2-4 和 @ 987654326@;我需要使用 path[0] 案例从 1 到 5;我现在是 2 并且代码显示:列表“路径”的索引 0 指向这个新列表 [1,2]+[4];这是一个死胡同,我上一个层次。自最新记录中的先前分配以来,“路径”的索引 0 如何指向 [1]。 call 告诉它指向 [1,2,4]?
猜你喜欢
  • 2020-03-11
  • 1970-01-01
  • 2022-12-17
  • 1970-01-01
  • 2021-04-03
  • 2022-01-13
  • 2016-07-13
  • 2020-10-03
相关资源
最近更新 更多