364

我正在尝试将一个较长的空心“数据”类转换为一个命名元组。我的班级目前看起来像这样:

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

但是这里有一个问题。我原来的类只允许我传入一个值,并通过使用命名/关键字参数的默认值来处理默认值。就像是:

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

但这在我重构的命名元组的情况下不起作用,因为它希望我传递所有字段。我当然可以替换 to 的出现,Node(val)但这Node(val, None, None)不符合我的喜好。

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

4

22 回答 22

658

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

或者更好的是,使用新的数据类库,它比命名元组好得多。

>>> 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 中,如果您设置的默认值少于命名元组中存在的默认值,则默认值将应用于最右边的参数。这允许您将一些参数保留为必需参数。

>>> 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)
于 2013-08-21T02:40:32.540 回答
151

我继承了 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)

这保留了直观的类型层次结构,而伪装成类的工厂函数的创建则没有。

于 2013-05-23T18:10:31.807 回答
102

将其包装在一个函数中。

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

def Node(val, left=None, right=None):
  return NodeT(val, left, right)
于 2012-07-05T20:10:15.687 回答
90

typing.NamedTuplePython 3.6.1+ 中,您可以为 NamedTuple 字段提供默认值和类型注释。typing.Any如果您只需要前者,请使用:

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)

此外,如果您需要默认值和可选的可变性,Python 3.7 将具有数据类(PEP 557),可以在某些(很多?)情况下替换命名元组。


旁注:Python 中当前注释规范(:参数和变量之后->的表达式以及函数之后的表达式)的一个怪癖是它们在定义时被评估*。因此,由于“一旦类的整个主体被执行,类名就被定义了”,'Node'上面类字段中的注解必须是字符串以避免出现 NameError。

这种类型提示称为“前向引用”([1][2]),并且使用PEP 563 , Python 3.7+ 将有一个__future__导入(在 4.0 中默认启用),允许使用前向引用没有报价,推迟他们的评估。

* AFAICT 仅在运行时不评估局部变量注释。(来源:PEP 526

于 2017-04-01T13:58:10.770 回答
21

这是直接来自文档的示例

可以通过使用 _replace() 自定义原型实例来实现默认值:

>>> Account = namedtuple('Account', 'owner balance transaction_count')
>>> default_account = Account('<owner name>', 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")

但是,我更喜欢这里给出的其他一些答案。为了完整起见,我只是想添加它。

于 2015-04-20T18:53:55.230 回答
19

我不确定仅使用内置的命名元组是否有简单的方法。有一个名为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)
于 2012-07-05T19:25:29.397 回答
15

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

from collections import namedtuple
from functools import partial

Node = namedtuple('Node', ('val left right'))
Node.__new__ = partial(Node.__new__, left=None, right=None)
于 2014-01-22T18:23:13.590 回答
15

在 python3.7+ 中有一个全新的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)
于 2018-02-01T17:36:06.067 回答
7

简短,简单,不会导致人们使用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))
于 2016-07-12T15:07:10.747 回答
6

defaultsPython 3.7:在命名元组定义中引入参数。

如文档中所示的示例:

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

在这里阅读更多。

于 2018-07-26T11:17:21.547 回答
5

一个稍微扩展的示例,用于初始化所有缺少的参数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)
于 2013-09-19T09:18:08.200 回答
4

结合@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'
于 2014-08-26T08:45:45.223 回答
4

你也可以使用这个:

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)
于 2012-10-17T17:34:08.873 回答
4

我发现这个版本更容易阅读:

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 并让函数执行替换行来更改它。

于 2015-01-06T22:31:18.760 回答
4

由于您将namedtuple其用作数据类,因此您应该知道 python 3.7 将@dataclass为此目的引入一个装饰器——当然它具有默认值。

文档中的一个示例

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

比 hacking 更干净、可读和可用namedtuple。不难预测namedtuples 的使用会随着 3.7 的采用而下降。

于 2018-05-24T09:18:04.570 回答
2

这个对不同问题的回答的启发,这是我提出的基于元类和使用的解决方案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)))
于 2018-02-24T17:31:23.287 回答
2

jterrace 使用recordtype 的答案很好,但是库的作者建议使用他的namedlist项目,它提供了mutable ( namedlist) 和immutable ( 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)
于 2018-10-05T11:21:24.887 回答
1

这是一个简短而简单的通用答案,它为带有默认参数的命名元组提供了很好的语法:

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
于 2018-07-17T18:04:05.803 回答
0

如果您想保留使用类型注释的可能性,不幸的是,@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 版本相同的实现
于 2021-04-05T06:55:51.680 回答
0

使用NamedTuple我的Advanced Enum (aenum)库中的类,并使用class语法,这非常简单:

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's answer

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)

是简单,也是metaclass基于而不是exec基于。

于 2016-11-30T15:02:17.197 回答
0

另一种解决方案:

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)
于 2017-03-27T11:06:20.967 回答
-1

这是 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)
于 2016-01-19T14:20:49.503 回答