3

I am trying to figure out what the difference is between

  1. path = path + [node1]
  2. path += [node1]
  3. path.append(node1)

I am getting the right path for path = path + [node1] but not the other two.

def find_path(self, node1, node2, path = []):
    """ Finds any path from node1 to node2 which may not be the shortest """

    path = path + [node1]
    if node1 == node2:
        return path
    if node1 not in self._graph:
        return None
    for node in self._graph[node1]:
        if node not in path:
            new_path = self.find_path(node, node2, path)
            if new_path:
                return new_path
    return None

1 Answers1

2

You need to be very careful when using mutable default arguments. Please see “Least Astonishment” and the Mutable Default Argument.

The reason for the difference in behaviour is that

path = path + [node1]

creates a new list and binds it to the name path. The other 2 alternatives modify the existing list bound to path.


As the linked question explains, default arguments are created when the function definition is compiled, not when the function is called. This is especially significant when using a mutable default arg in a recursive function because it implies that the default arg is not reset on each top-level recursive call.

If you don't want the "special" behaviour that using a mutable default arg gives you, you can do something like:

def find_path(self, node1, node2, path=None):
    if path is None:
        path = []
    # rest of code

If None is a valid arg for path then you'll need to use some other sentinel, eg

sentinel = object()
def find_path(self, node1, node2, path=sentinel):
    if path is sentinel:
        path = []
    # rest of code

Here's a short demo that illustrates the "special" behaviour of a mutable default arg. You can see that lst remembers its previous contents.

def test(a, lst=[]):
    lst += [a]
    print(a, lst)

for i in range(5):
    test(i)

output

0 [0]
1 [0, 1]
2 [0, 1, 2]
3 [0, 1, 2, 3]
4 [0, 1, 2, 3, 4]

In contrast, using lst = lst + [a], we create a new list rather than appending to the default list.

def test(a, lst=[]):
    lst = lst + [a]
    print(a, lst)

for i in range(5):
    test(i)

output

0 [0]
1 [1]
2 [2]
3 [3]
4 [4]
Community
  • 1
  • 1
PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
  • That explains it pretty well, thanks so much! So the path there is local while the path for the other two is global in the sense that they affect the previous recursion calls? – Sandeep Chowdary Dec 03 '16 at 08:39
  • @SandeepChowdary> it's just that with python, if using a mutable object such as `[]` as default argument, it is created once at the beginning of your program and then re-used over and over again. Thus subsequent calls will see whatever modifications previous calls made to it. It is controversial design, as it goes opposite of most other languages, ending up being a recurrent trap for python beginners (and some more experienced devs as well) – spectras Dec 03 '16 at 08:50
  • Thanks guys that explains it so well! – Sandeep Chowdary Dec 03 '16 at 17:31