sasuke sasuke - 6 months ago 100
Python Question

Named tuple and optional keyword arguments

I'm trying to convert a longish hollow "data" class into a named tuple. My class currently looks like this:

class Node(object):
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right

After conversion to
it looks like:

from collections import namedtuple
Node = namedtuple('Node', 'val left right')

But there is a problem here. My original class allowed me to pass in just a value and took care of the default by using default values for the named/keyword arguments. Something like:

class BinaryTree(object):
def __init__(self, val):
self.root = Node(val)

But this doesn't work in the case of my refactored named tuple since it expects me to pass all the fields. I can of course replace the occurrences of
Node(val, None, None)
but it isn't to my liking.

So does there exist a good trick which can make my re-write successful without adding a lot of code complexity (metaprogramming) or should I just swallow the pill and go ahead with the "search and replace"? :)


-- sauke


Set Node.__new__.__defaults__ (or Node.__new__.func_defaults before Python 2.6) to the default values.

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

You can also have required fields by making the __defaults__ list shorter.

>>> Node.__new__.__defaults__ = (None, None)
>>> Node()
Traceback (most recent call last):
TypeError: __new__() missing 1 required positional argument: 'val'
>>> Node(3)
Node(val=3, left=None, right=None)


Here's a nice wrapper for you, which even lets you (optionally) set the default values to something other than None. (This does not support required arguments.):

import collections
def namedtuple_with_defaults(typename, field_names, default_values=()):
    T = collections.namedtuple(typename, field_names)
    T.__new__.__defaults__ = (None,) * len(T._fields)
    if isinstance(default_values, collections.Mapping):
        prototype = T(**default_values)
        prototype = T(*default_values)
    T.__new__.__defaults__ = tuple(prototype)
    return T


>>> Node = namedtuple_with_defaults('Node', 'val left right')
>>> Node()
Node(val=None, left=None, right=None)
>>> Node = namedtuple_with_defaults('Node', 'val left right', [1, 2, 3])
>>> Node()
Node(val=1, left=2, right=3)
>>> Node = namedtuple_with_defaults('Node', 'val left right', {'right':7})
>>> Node()
Node(val=None, left=None, right=7)
>>> Node(4)
Node(val=4, left=None, right=7)