namedtuple and default values for optional keyword arguments
我正在尝试将一个longish中空的"data"类转换为一个命名的元组。我的班级目前如下:
1 2 3 4 5 | class Node(object): def __init__(self, val, left=None, right=None): self.val = val self.left = left self.right = right |
在转换为
1 2 | from collections import namedtuple Node = namedtuple('Node', 'val left right') |
但这里有个问题。我的原始类只允许我传入一个值,并通过为命名/关键字参数使用默认值来处理默认值。类似:
1 2 3 | class BinaryTree(object): def __init__(self, val): self.root = Node(val) |
但对于重构的命名元组,这不起作用,因为它期望我传递所有字段。当然,我可以把
那么,有没有一个好的技巧可以使我的重写成功而不增加很多代码复杂性(元编程),或者我应该吞下药丸继续"搜索和替换"?:)
Python 3.7
使用默认参数。
1 2 3 4 5 | >>> from collections import namedtuple >>> fields = ('val', 'left', 'right') >>> Node = namedtuple('Node', fields, defaults=(None,) * len(fields)) >>> Node() Node(val=None, left=None, right=None) |
在python 3.7之前
将
1 2 3 4 5 | >>> 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之前
将
1 2 3 4 5 | >>> 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中的默认值,则默认值将应用于最右边的参数。这允许您保留一些参数作为必需的参数。
1 2 3 4 5 6 7 | >>> 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的包装器
这里有一个包装器,它甚至允许您(可选)将默认值设置为除
1 2 3 4 5 6 7 8 9 10 | 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 |
例子:
1 2 3 4 5 6 7 8 9 10 11 | >>> 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) |
我把名字分为两个子类,然后超越了
1 2 3 4 5 6 | 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) |
这保留了一个直观的类型层次结构,而伪装为类的工厂函数的创建则没有。
用函数包装它。
1 2 3 4 | NodeT = namedtuple('Node', 'val left right') def Node(val, left=None, right=None): return NodeT(val, left, right) |
使用python 3.6.1+中的
1 2 3 4 5 6 7 | from typing import Any, NamedTuple class Node(NamedTuple): val: Any left: 'Node' = None right: 'Node' = None |
用途:
1 2 3 4 5 | >>> 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)。cases替换namedtuples。旁注:在python中,注释(参数和变量的
这种类型提示称为"转发引用"([1],[2]),使用PEP 563,python 3.7+将有一个
*afaict只有局部变量注释不会在运行时进行计算。(来源:PEP 526)
这是一个直接来自文档的示例:
Default values can be implemented by using _replace() to customize a
prototype instance:
1
2
3
4 >>> 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的例子是:
1 2 3 4 | from collections import namedtuple Node = namedtuple('Node', 'val left right') default_node = Node(None, None, None) example = default_node._replace(val="whut") |
不过,我更喜欢这里给出的其他一些答案。为了完整起见,我只想添加这个。
我不确定是否有一个简单的方法只与内置的名称双重。有一个名为recordtype的好模块具有以下功能:
1 2 3 4 5 6 | >>> 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) |
以下是一个更紧凑的版本,灵感来自于Justinfay的答案:
1 2 3 4 5 | from collections import namedtuple from functools import partial Node = namedtuple('Node', ('val left right')) Node.__new__ = partial(Node.__new__, left=None, right=None) |
在python3.7+中有一个全新的默认值=关键字参数。
defaults can be
None or an iterable of default values. Since fields with a default value must come after any fields without a default, the defaults are applied to the rightmost parameters. For example, if the fieldnames are['x', 'y', 'z'] and the defaults are(1, 2) , thenx will be a required argument,y will default to1 , andz will default to2 .
示例用法:
1 2 3 4 5 6 7 8 9 10 11 12 | $ ./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) |
简短、简单,不会导致人们不正确地使用
1 2 3 4 5 6 7 8 | 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)) |
用
1 2 3 4 5 6 7 8 | 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) |
结合@denis和@mark的方法:
1 2 3 4 5 6 7 8 9 | 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) |
这应该支持使用位置参数和混合大小写创建元组。测试用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | >>> 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) |
但也支持类型错误:
1 2 | >>> Node(1, left=2) TypeError: __new__() got multiple values for keyword argument 'left' |
您还可以使用:
1 2 3 4 5 6 7 8 | 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) |
这基本上为您提供了使用默认值构造任何命名元组的可能性,并仅覆盖您需要的参数,例如:
1 2 3 4 5 6 7 8 | 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) |
python 3.7:在namedtuple定义中引入
如文档所示的示例:
1 2 3 4 5 | >>> Account = namedtuple('Account', ['type', 'balance'], defaults=[0]) >>> Account._fields_defaults {'balance': 0} >>> Account('premium') Account(type='premium', balance=0) |
在这里阅读更多。
我觉得这个版本更容易阅读:
1 2 3 4 5 6 7 8 9 10 | 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行来更改这一点。
因为您使用
文档示例:
1 2 3 4 | @dataclass class C: a: int # 'a' has no default value b: int = 0 # assign a default value for 'b' |
比黑客攻击
jterrace使用recordtype的答案很好,但库的作者建议使用他的命名列表项目,该项目提供可变(
1 2 3 4 5 6 | 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 2 3 4 5 6 7 8 9 10 | 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__ = () |
然后:
1 2 | >>> 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))) |
使用我的
1 2 3 4 5 6 | from aenum import NamedTuple class Node(NamedTuple): val = 0 left = 1, 'previous Node', None right = 2, 'next Node', None |
一个潜在的缺点是,对于任何具有默认值的属性(对于简单属性,它是可选的),都需要一个
1 2 3 4 5 6 7 | >>> Node() Traceback (most recent call last): ... TypeError: values not provided for field(s): val >>> Node(3) Node(val=3, left=None, right=None) |
与
1 2 3 4 5 6 | 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) |
是简单的,也是基于
另一个解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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 |
用途:
1 2 3 4 5 6 7 8 9 10 11 12 | >>> 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 2 3 4 5 6 7 | 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 |
用途:
1 2 | Test = dnamedtuple('Test', 'one two three', two=2) Test(1, 3) # Test(one=1, three=3, two=2) |
Minified:
1 2 3 4 5 | 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 |
这里有一个不太灵活但更简洁的MarkLodato包装器版本:它将字段和默认值作为字典。
1 2 3 4 5 | 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 |
例子:
1 2 3 4 5 6 7 8 9 10 11 12 | 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) |