Mutable default argument for a Python namedtuple

Question:

I came across a neat way of having namedtuples use default arguments from here.

from collections import namedtuple
Node = namedtuple('Node', 'val left right')
Node.__new__.__defaults__ = (None, None, None)
Node()

Node(val=None, left=None, right=None)

What would you do if you would want the default value for ‘right’ to be a empty list? As you may know, using a mutable default argument such as a list is a no no.

Is there a simple way to implement this?

Asked By: ChaimG

||

Answers:

You can’t do that that way, because the values in __defaults__ are the actual default values. That is, if you wrote a function that did had someargument=None, and then checked inside the function body with someargument = [] if someargument is None else someargument or the like, the corresponding __defaults__ entry would still be None. In other words, you can do that with a function because in a function you can write code to do whatever you want, but you can’t write custom code inside a namedtuple.

But if you want default values, just make a function that has that logic and then creates the right namedtuple:

def makeNode(val=None, left=None, right=None):
    if right is None:
        val = []
    return Node(val, left, right)
Answered By: BrenBarn

The way given in the accepted answer works great. The only downside I see is that one has to both know (in the case of some other user) and remember to use the factory function instead of the named tuple class- both when creating the object, and when doing things like this:

isinstance(node, Node) #  success
isinstance(node, makeNode) #  misery

A way around this problem might be to do something like what is shown below.

NodeBase = nt('NodeBase', 'val left right')
NodeBase.__new__.__defaults__ = (None, None, None)

class Node(NodeBase):
    '''A namedtuple defined as:

    Node(val, left, right)

    with default values of (None, None, [])'''
    __slots__ = ()
    def __new__(cls, *args, **kwargs):
        obj = super().__new__(cls, *args, **kwargs)
            if obj.right is None:
                obj = obj._replace(right = [])
            return obj
Answered By: Rick

Just small change in implementation from Rick Teachey, the default value can be set outside class:

NodeBase = namedtuple('NodeBase', 'val left right')

class Node(NodeBase):
    __slots__ = ()
    def __new__(cls, *, right=[], **kwargs):
        obj = super().__new__(cls, right=right, **kwargs)
        return obj

#IMPLEMENTATION
kw = {'val': 1, 'left':12}

m  = Node(**kw) 
# outputs Node(val=1, left=12, right=[])
Answered By: Bravhek

Since this question has been asked, the dataclasses module has been proposed and accepted into Python. This module has a lot of overlapping use cases with namedtuples but with some more flexibility and power. In particular, you can specify a factory function for when you want to specify a default for a mutable field.

from typing import List
from dataclasses import dataclass, field

@dataclass
class Node:
    val: str
    left: List["Node"] = field(default_factory=list)
    right: List["Node"] = field(default_factory=list)

In a data class you specify the types of the various fields, so in this case I had to fill in a few blanks and assume that val would be a string and that left and right would both be lists of other Node objects.

Since right and left are the left hand side of an assignment in the class definition, they are optional arguments when we initialize a Node object. Further, we could supply a default value, but instead we supplied a default factory, which is a function that is called with 0 arguments whenever we initialize a Node object without specifying those fields.

For example:

node_1 = Node('foo')
# Node(val='foo', left=[], right=[])

node_2 = Node('bar', left=[node_1])
# Node(val='bar', left=[Node(val='foo', left=[], right=[])], right=[])

node_3 = Node('baz')
# Node(val='baz', left=[], right=[])

node_4 = Node('quux', left=[node_2], right=[node_3])
# Node(val='quux', left=[Node(val='bar', left=[Node(val='foo', left=[], right=[])], right=[])], right=[Node(val='baz', left=[], right=[])])

Personally I find myself reaching for dataclasses over namedtuples for any application where I need more than just the thinnest container for data.

Answered By: Kyle Parsons
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.