namedtuple和可选关键字参数的默认值


300

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

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)

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

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


2
您为什么要进行此转换?我喜欢你原来的Node课程。为什么要转换为命名元组?
steveha,2012年

34
我想进行此转换,因为当前Node类和其他类是具有多个不同字段的简单数据持有者值对象(Node只是其中之一)。这些类声明只不过是行噪恕我直言,因此希望对其进行修剪。为什么要维护不需要的东西?:)
sasuke 2012年

您的类上根本没有任何方法函数?例如,您没有一种.debug_print()方法可以遍历树并打印出来吗?
2012年

2
当然可以,但这是给BinaryTree全班的。Node考虑到命名元组具有体面__str____repr__代表性,其他数据持有人不需要特别的方法,尤其是。:)
sasuke 2012年

好吧,似乎很合理。而且我认为Ignacio Vazquez-Abrams为您提供了答案:使用为您的节点执行默认值的函数。
steveha 2012年

Answers:


532

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)

或者更好的是,使用新的dataclasses库,它比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)

22
让我们看一下...你的单线:a)是最短/最简单的答案,b)保持空间效率,c)不会破坏isinstance...所有优点,没有缺点...太糟糕了派对。这是最好的答案。
格拉特,2014年

1
包装器版本的一个问题:与内置的collections.namedtuple不同,如果将def()包含在其他模块中,则该版本不能进行腌制/多进程可序列化。
Michael Scott Cuthbert 2014年

2
我给了这个答案,因为它比我自己的更可取。可惜的是,我自己的答案一直被否决:|
贾斯汀·菲

3
@ishaaq,问题在于那(None)不是一个元组None。如果您改用(None,)它,则应该可以正常工作。
Mark Lodato

2
优秀的!您可以使用以下方法来推广默认设置:Node.__new__.__defaults__= (None,) * len(Node._fields)
ankostis 2015年

142

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

这样可以保留直观的类型层次结构,而伪装成类的工厂函数则不会创建。


7
为了保持命名元组的空间效率,可能需要使用slot和field属性。
Pepijn 2014年

由于某种原因,__new___replace使用时不被调用。

1
请查看@ marc-lodato答案,下面的IMHO比这更好。
贾斯汀·菲

1
但是@ marc-lodato的答案并未提供子类具有不同默认值的能力
Jason S

1
@JasonS,我怀疑子类具有不同的默认值可能会违反LSP。但是,子类很可能希望拥有更多默认值。无论如何,子类都可以使用justinfay的方法,而基类可以使用Marc的方法
Alexey '18

94

将其包装在函数中。

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

def Node(val, left=None, right=None):
  return NodeT(val, left, right)

15
这很聪明,可能是一个不错的选择,但也会因破坏而引起问题isinstance(Node('val'), Node):它现在将引发异常,而不是返回True。@justinfay的答案稍微冗长一些,如下所示,它正确地保留了类型层次结构信息,因此,如果其他人将要与Node实例进行交互,则这可能是一种更好的方法。
加百利·格兰特

4
我喜欢这个答案的简洁。也许可以通过命名函数def make_node(...):而不是假装它是类定义来解决上面注释中的问题。这样,用户就不会试图检查函数的类型多态性,而是使用元组定义本身。
user1556435 '16

请参阅我的答案,以免受到误导人们isinstance误用的困扰。
Elliot Cameron

70

随着typing.NamedTuple在Python 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),这些数据类在某些(很多情况下可以替换namedtuple。


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

这种类型的提示称为“正向引用”([1][2]),在PEP 563中, Python 3.7+将具有__future__导入(默认情况下在4.0中启用),该导入将允许使用正向引用没有报价,则推迟评估。

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


4
对于3.6.1+用户来说,这似乎是最干净的解决方案。请注意,此示例作为字段的类型提示是(有点)令人困惑,left并且right(即Node)与要定义的类的类型相同,因此必须以字符串形式编写。
101:

1
@ 101,谢谢,我已经在答案中添加了关于此的注释。
和尚时间

2
这个成语的模拟是my_list: List[T] = None self.my_list = my_list if my_list is not None else []什么?我们不能使用这样的默认参数吗?
weberc2 '18

@ weberc2很好的问题!我不确定这种可变def的解决方法。可以使用值typing.NamedTuple。但是,通过数据类,您可以将 Field对象与default_factoryattr 一起使用。为此,请用替换您的习惯用语my_list: List[T] = field(default_factory=list)
和尚时间

20

这是直接来自docs的示例

可以使用_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")

但是,我更喜欢这里给出的其他一些答案。我只是想添加此内容以保持完整性。


2
+1。这是非常奇怪的是,他们决定用走_法(基本上是指私人的)这样的事情replace,这似乎非常有用的..
佐助

@sasuke-我也想知道。您用空格分隔的字符串而不是定义元素已经有点奇怪了*args。可能只是在许多东西标准化之前,它已被添加到语言中。
蒂姆·蒂斯达尔

12
_前缀是为了避免与用户定义的元组字段的名称发生冲突(相关文档引号:“除以下划线开头的名称外,任何有效的Python标识符都可以用于字段名”)。至于以空格分隔的字符串,我认为这只是为了节省一些击键(并且您可以根据需要传递一系列字符串)。
索伦Løvborg

1
嗯,是的,我忘记了您将命名的元组的元素作为属性来访问,因此这_很有意义。
蒂姆·提斯多

2
您的解决方案既简单又最佳。其余的恕我直言,相当丑陋。我只会做一个小小的改变。我希望使用node_default而不是default_node,因为它可以提供更好的IntelliSense体验。万一您开始键入node,您会收到所需的一切:)
Pavel Hanpari 2016年

19

我不确定仅内置的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)

2
嗯,虽然recordtype将来的工作看起来很有趣,但无法使用第三方软件包。+1
佐助

该模块非常小,只有一个文件,因此您始终可以将其添加到项目中。
jterrace 2012年

公平地说,尽管我将等待更多时间以使用纯命名元组解决方案,但是在标记为已接受之前,还有一个解决方案!:)
sasuke 2012年

同意的纯Python会很好,但是我不认为有一个:(
jterrace

3
只是要注意,这recordtype是可变的,而namedtuple不是。如果您希望对象是可哈希的(这可能是不正确的,因为它最初是一个类),则这可能很重要。
bavaza 2013年

14

这是一个受Justinfay的回答启发的更紧凑的版本:

from collections import namedtuple
from functools import partial

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

7
请注意,Node(1, 2)该方法不适用于此食谱,但可以在@justinfay的答案中使用。否则,它很漂亮(+1)。
jorgeca 2014年

12

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

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

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)

5

Python 3.7:的介绍 defaults在namedtuple定义中 param。

文档中显示的示例:

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

在这里阅读更多。


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)

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'

3

我发现此版本更易于阅读:

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


3

由于您是namedtuple作为数据类使用的,因此应注意python 3.7 @dataclass为此会引入一个装饰器-当然,它具有默认值。

来自docs的示例

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

比黑客更干净,可读性和可用性更高namedtuple。不难预测,namedtuple随着3.7的采用,s的使用将下降。


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

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)

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

0

使用NamedTupleAdvanced 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基础。


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)

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

4
dict无法保证订购。
伊桑·弗曼
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.