可选关键字参数的命名元组和默认值

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

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),但这不是我喜欢的。

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

178533 次浏览

我不确定是否有一个简单的方法,只有内置的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)

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

使用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)
这应该支持创建带有位置参数和混合大小写的元组。 测试用例:< / p >
>>> 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()自定义a 原型实例:< / p >

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

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

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

使用我的Advanced Enum (aenum)库中的NamedTuple类,并使用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

另一个解决方案:

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+中的typing.NamedTuple,您可以为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中,当前注释规范(参数和变量在:之后,函数在->之后的表达式)的一个奇怪之处在于,它们在定义时间__abc4时被求值。因此,由于“类名在类的整个主体被执行后才被定义”,所以上面类字段中'Node'的注释必须是字符串,以避免NameError.

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

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

在python3.7+中,有一个全新的违约率=关键字参数。

违约可以是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:在命名元组定义中引入defaults参数。

示例如文档所示:

>>> 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. 使用dataclass >= 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使数据类不可变。