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

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。有一个很好的模块叫做recordtype,它有这个功能:

>>> from recordtype import recordtype
>>> Node = recordtype('Node', [('val', None), ('left', None), ('right', None)])
>>> Node(3)
Node(val=3, left=None, right=None)
>>> Node(3, 'L')
Node(val=3, left=L, right=None)

将其包装在函数中。

NodeT = namedtuple('Node', 'val left right')

def Node(val, left=None, right=None):
  return NodeT(val, 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)

我子类化了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)

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


Python 3.7

使用默认参数。

>>> from collections import namedtuple
>>> fields = ('val', 'left', 'right')
>>> Node = namedtuple('Node', fields, defaults=(None,) * len(fields))
>>> Node()
Node(val=None, left=None, right=None)

或者更好的是,使用新的数据类库,它比namedtuple要好得多。

>>> from dataclasses import dataclass
>>> from typing import Any
>>> @dataclass
... class Node:
...     val: Any = None
...     left: 'Node' = None
...     right: 'Node' = None
>>> Node()
Node(val=None, left=None, right=None)

Python 3.7之前

设置Node.__new__。__defaults__为默认值。

>>> 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)

Python 2.6之前

设置Node.__new__。Func_defaults为默认值。

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

订单

在所有版本的Python中,如果您设置的默认值少于namedtuple中存在的默认值,则默认值将应用于最右边的形参。这允许您保留一些参数作为必需的参数。

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

Python 2.6至3.6的包装器

这里有一个包装器,它甚至允许您(可选地)将默认值设置为None以外的值。这并不支持所需的参数。

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)
    else:
        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)

一个稍微扩展的例子,用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)

以下是一个更紧凑的版本,灵感来自justinfay的回答:

from collections import namedtuple
from functools import partial

Node = namedtuple('Node', ('val left right'))
Node.__new__ = partial(Node.__new__, left=None, right=None)

结合@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'

我觉得这个版本更容易读:

from collections import namedtuple

def my_tuple(**kwargs):
    defaults = {
        'a': 2.0,
        'b': True,
        'c': "hello",
    }
    default_tuple = namedtuple('MY_TUPLE', ' '.join(defaults.keys()))(*defaults.values())
    return default_tuple._replace(**kwargs)

这并不高效,因为它需要创建两次对象,但你可以通过在模块内定义默认的duple并让函数执行replace行来改变这一点。


这是一个直接来自文档的例子:

默认值可以通过使用_replace()来实现 原型实例: >>> Account = namedtuple('Account', 'owner balance transaction_count') >>> default_account =帐户('<所有者名称>',0.0,0) >>> johns_account = default_account._replace(owner='John') >>> janes_account = default_account._replace(owner='Jane')

所以,OP的例子是:

from collections import namedtuple
Node = namedtuple('Node', 'val left right')
default_node = Node(None, None, None)
example = default_node._replace(val="whut")

然而,我更喜欢这里给出的其他一些答案。为了完整起见,我想加上这个。


下面是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)

短的,简单的,不会导致人们不恰当地使用isinstance:

class Node(namedtuple('Node', ('val', 'left', 'right'))):
    @classmethod
    def make(cls, val, left=None, right=None):
        return cls(val, left, right)

# Example
x = Node.make(3)
x._replace(right=Node.make(4))

使用我的高级Enum (aenum)库中的NamedTuple类,并使用类语法,这是相当简单的:

from aenum import NamedTuple

class Node(NamedTuple):
    val = 0
    left = 1, 'previous Node', None
    right = 2, 'next Node', None

一个潜在的缺点是,任何具有默认值的属性都需要__doc__字符串(对于简单属性是可选的)。在实际使用中是这样的:

>>> Node()
Traceback (most recent call last):
  ...
TypeError: values not provided for field(s): val

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

这比justinfay的答案更有优势:

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)

是简单,以及是基于元类而不是基于exec。


另一个解决方案:

import collections


def defaultargs(func, defaults):
    def wrapper(*args, **kwargs):
        for key, value in (x for x in defaults[len(args):] if len(x) == 2):
            kwargs.setdefault(key, value)
        return func(*args, **kwargs)
    return wrapper


def namedtuple(name, fields):
    NamedTuple = collections.namedtuple(name, [x[0] for x in fields])
    NamedTuple.__new__ = defaultargs(NamedTuple.__new__, [(NamedTuple,)] + fields)
    return NamedTuple

用法:

>>> Node = namedtuple('Node', [
...     ('val',),
...     ('left', None),
...     ('right', None),
... ])
__main__.Node

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

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

在打字。在Python 3.6.1+中,您可以为NamedTuple字段提供默认值和类型注释。使用打字。如果你只需要前者:

from typing import Any, NamedTuple


class Node(NamedTuple):
    val: Any
    left: 'Node' = None
    right: 'Node' = None

用法:

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

Also, in case you need both default values and optional mutability, Python 3.7 is going to have data classes (PEP 557) that can in some (many?) cases replace namedtuples. Sidenote: one quirk of the current specification of annotations (expressions after : for parameters and variables and after -> for functions) in Python is that they are evaluated at definition time*. So, since "class names become defined once the entire body of the class has been executed", the annotations for 'Node' in the class fields above must be strings to avoid NameError.

这种类型提示被称为“前向引用”([1],[2]),在PEP 563中,Python 3.7+将有__future__导入(在4.0中默认启用),允许使用不带引号的前向引用,推迟它们的计算。

* AFAICT只有局部变量注释不计算在运行时。(来源:PEP 526)


在python3.7+中,有一个全新的defaults= keyword参数。

defaults可以是None或包含默认值的可迭代对象。由于具有默认值的字段必须出现在任何没有默认值的字段之后,因此默认值应用于最右边的参数。例如,如果字段名是['x', 'y', 'z'],并且默认值是(1,2),那么x将是必选参数,y将默认值为1,z将默认值为2。

使用示例:

$ ./python
Python 3.7.0b1+ (heads/3.7:4d65430, Feb  1 2018, 09:28:35) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from collections import namedtuple
>>> nt = namedtuple('nt', ('a', 'b', 'c'), defaults=(1, 2))
>>> nt(0)
nt(a=0, b=1, c=2)
>>> nt(0, 3)  
nt(a=0, b=3, c=2)
>>> nt(0, c=3)
nt(a=0, b=1, c=3)

受到这个对另一个问题的回答的启发,下面是我提出的基于元类并使用super(正确处理未来的子类)的解决方案。这和justinfay的答案很相似。

from collections import namedtuple

NodeTuple = namedtuple("NodeTuple", ("val", "left", "right"))

class NodeMeta(type):
    def __call__(cls, val, left=None, right=None):
        return super(NodeMeta, cls).__call__(val, left, right)

class Node(NodeTuple, metaclass=NodeMeta):
    __slots__ = ()

然后:

>>> Node(1, Node(2, Node(4)),(Node(3, None, Node(5))))
Node(val=1, left=Node(val=2, left=Node(val=4, left=None, right=None), right=None), right=Node(val=3, left=None, right=Node(val=5, left=None, right=None)))

由于您正在使用namedtuple作为数据类,您应该注意到python 3.7将为此目的引入@dataclass装饰器——当然它有默认值。

文档中的一个例子:

@dataclass
class C:
    a: int       # 'a' has no default value
    b: int = 0   # assign a default value for 'b'

比破解namedtuple更干净,可读和可用。不难预测,随着3.7的采用,namedtuples的使用将会减少。


下面是一个简短、简单的通用答案,对于带默认参数的命名元组,它有一个很好的语法:

import collections

def dnamedtuple(typename, field_names, **defaults):
    fields = sorted(field_names.split(), key=lambda x: x in defaults)
    T = collections.namedtuple(typename, ' '.join(fields))
    T.__new__.__defaults__ = tuple(defaults[field] for field in fields[-len(defaults):])
    return T

用法:

Test = dnamedtuple('Test', 'one two three', two=2)
Test(1, 3)  # Test(one=1, three=3, two=2)

缩小:

def dnamedtuple(tp, fs, **df):
    fs = sorted(fs.split(), key=df.__contains__)
    T = collections.namedtuple(tp, ' '.join(fs))
    T.__new__.__defaults__ = tuple(df[i] for i in fs[-len(df):])
    return T

Python 3.7:在namedtuple定义中引入默认参数。

示例如文档所示:

>>> Account = namedtuple('Account', ['type', 'balance'], defaults=[0])
>>> Account._fields_defaults
{'balance': 0}
>>> Account('premium')
Account(type='premium', balance=0)

点击这里阅读更多。


jterrace给出的使用recordtype的答案很好,但是库的作者建议使用他的namedlist项目,该项目同时提供了可变(namedlist)和不可变(namedtuple)实现。

from namedlist import namedtuple
>>> Node = namedtuple('Node', ['val', ('left', None), ('right', None)])
>>> Node(3)
Node(val=3, left=None, right=None)
>>> Node(3, 'L')
Node(val=3, left=L, right=None)

如果你想保留使用类型注释的可能性,不幸的是,@mark-lodato非常好的解决方案是不可用的(它失败于我设置__defaults__)。 另一种方法是使用attrs:

import attr

 
@attr.s
class Node(object):
    val: str = attr.ib()
    left: 'Node' = attr.ib(None)
    right: 'Node' = attr.ib(None)

这有:

类型注解 漂亮的__str__和__repr__ 可定制的,因为它是一个真正的类 所有Python版本的实现都相同


1. 使用NamedTuple >= Python 3.6

从Python 3.7+开始,您可以从支持默认值的typing模块中使用NamedTuple。

https://docs.python.org/3/library/typing.html#typing.NamedTuple

from typing import NamedTuple

class Employee(NamedTuple):
    name: str
    id: int = 3

employee = Employee('Guido')
assert employee.id == 3

注意:虽然NamedTuple在类语句中作为超类出现,但它实际上不是。打字。NamedTuple使用元类的高级功能来自定义用户类的创建。

issubclass(Employee, typing.NamedTuple)
# return False
issubclass(Employee, tuple)
# return True

2. 使用数据类>= Python 3.7

from dataclasses import dataclass

@dataclass(frozen=True)
class Employee:
    name: str
    id: int = 3

employee = Employee('Guido')
assert employee.id == 3

frozen=True使数据类不可变。