我试图将一个较长的中空“数据”类转换为命名元组。我的类目前看起来是这样的:

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

转换为namedtuple后,它看起来像:

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

但这里有一个问题。我最初的类允许我只传入一个值,并通过为named/keyword参数使用默认值来处理默认值。喜欢的东西:

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

但这在重构的命名tuple中不起作用,因为它期望我传递所有字段。我当然可以替换Node(val)到Node(val, None, None)的出现,但这不是我喜欢的。

那么,是否存在一个好技巧,可以让我的重写成功,而不增加大量的代码复杂性(元编程),或者我应该吞下药丸,继续“搜索和替换”?:)


当前回答

我子类化了namedtuple并重写了__new__方法:

from collections import namedtuple

class Node(namedtuple('Node', ['value', 'left', 'right'])):
    __slots__ = ()
    def __new__(cls, value, left=None, right=None):
        return super(Node, cls).__new__(cls, value, left, right)

这保留了直观的类型层次结构,而创建伪装成类的工厂函数则无法做到这一点。

其他回答

我子类化了namedtuple并重写了__new__方法:

from collections import namedtuple

class Node(namedtuple('Node', ['value', 'left', 'right'])):
    __slots__ = ()
    def __new__(cls, value, left=None, right=None):
        return super(Node, cls).__new__(cls, value, left, right)

这保留了直观的类型层次结构,而创建伪装成类的工厂函数则无法做到这一点。

你也可以用这个:

import inspect

def namedtuple_with_defaults(type, default_value=None, **kwargs):
    args_list = inspect.getargspec(type.__new__).args[1:]
    params = dict([(x, default_value) for x in args_list])
    params.update(kwargs)

    return type(**params)

这基本上让你有可能构造任何带有默认值的命名元组,并只覆盖你需要的参数,例如:

import collections

Point = collections.namedtuple("Point", ["x", "y"])
namedtuple_with_defaults(Point)
>>> Point(x=None, y=None)

namedtuple_with_defaults(Point, x=1)
>>> Point(x=1, y=None)

下面是Mark Lodato的包装器的一个不太灵活但更简洁的版本:它将字段和默认值作为字典。

import collections
def namedtuple_with_defaults(typename, fields_dict):
    T = collections.namedtuple(typename, ' '.join(fields_dict.keys()))
    T.__new__.__defaults__ = tuple(fields_dict.values())
    return T

例子:

In[1]: fields = {'val': 1, 'left': 2, 'right':3}

In[2]: Node = namedtuple_with_defaults('Node', fields)

In[3]: Node()
Out[3]: Node(val=1, left=2, right=3)

In[4]: Node(4,5,6)
Out[4]: Node(val=4, left=5, right=6)

In[5]: Node(val=10)
Out[5]: Node(val=10, left=2, right=3)

结合@Denis和@Mark的方法:

from collections import namedtuple
import inspect

class Node(namedtuple('Node', 'left right val')):
    __slots__ = ()
    def __new__(cls, *args, **kwargs):
        args_list = inspect.getargspec(super(Node, cls).__new__).args[len(args)+1:]
        params = {key: kwargs.get(key) for key in args_list + kwargs.keys()}
        return super(Node, cls).__new__(cls, *args, **params) 

这应该支持使用位置参数和混合情况创建元组。 测试用例:

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

>>> print Node(1,2,3)
Node(left=1, right=2, val=3)

>>> print Node(1, right=2)
Node(left=1, right=2, val=None)

>>> print Node(1, right=2, val=100)
Node(left=1, right=2, val=100)

>>> print Node(left=1, right=2, val=100)
Node(left=1, right=2, val=100)

>>> print Node(left=1, right=2)
Node(left=1, right=2, val=None)

但也支持TypeError:

>>> Node(1, left=2)
TypeError: __new__() got multiple values for keyword argument 'left'

一个稍微扩展的例子,用None初始化所有缺失的参数:

from collections import namedtuple

class Node(namedtuple('Node', ['value', 'left', 'right'])):
    __slots__ = ()
    def __new__(cls, *args, **kwargs):
        # initialize missing kwargs with None
        all_kwargs = {key: kwargs.get(key) for key in cls._fields}
        return super(Node, cls).__new__(cls, *args, **all_kwargs)