2

I'm creating a simple tree where each node has any number of children in Python, and I created a Node class to help me. Each node holds a reference to its parent node (int), and any children nodes (list).

However, explicitly adding an empty list to the Node constructor's argument gave me strange results, and I'd love an explanation as to why this behaviour changes when the list is explicit or not explicitly put in the constructor arguments:

Implementation #1:

class Node:
    def __init__(self, value, parent, children=[]):
        self.parent = parent
        self.value = value 
        self.children = children

Implementation #2:

class Node:
    def __init__(self, value, parent):
        self.parent = parent
        self.value = value 
        self.children = []

To populate the 'nodes' array:

parents = [4,-1,4,1,1]
nodes = [None] * n
for i in range(n):
    nodes[i] = Node(i, parents[i])

To store the parent attribute of each node:

tree = Tree()
for i, node in enumerate(nodes):
    parent_id = node.parent
    if parent_id == -1: 
        tree.root = nodes[i]
    else:
        nodes[parent_id].children.append(node.value)

print([(node.value, node.children) for node in nodes])

With Implementation #1 I get:

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

but with Implementation #2 I (correctly) get:

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

Why the difference? I don't understand why the list is fully populated for each node even with the if and else statements. All help appreciated, including if you think there are better ways to do this.

Roshan
  • 23
  • 2

1 Answers1

1

Default arguments are bound once when the function is defined, so every object of Node gets the same list object in your first implementation.

Locals are evaluated when the function is run, so self.children=[] assigns a new list in each object.

A better approach if you want to allow an optional children argument would be to

class Node:
    def __init__(self, value, parent, children=None):
        self.parent = parent
        self.value = value 
        self.children = children or []

This uses None as the default value. The or operator allows us to select children if the argument is truthy, and an empty list if it is falsey.

From the docs.

Pranav Hosangadi
  • 23,755
  • 7
  • 44
  • 70
  • Incredibly quick response, and very clear. Thank you :) Is there a name for this general idea, e.g. when binding of arguments occurs? – Roshan May 13 '21 at 15:17
  • Here: https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument – Pranav Hosangadi May 13 '21 at 15:19