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

Python 可选关键字参数的命名元组和默认值,python,default-value,namedtuple,optional-arguments,Python,Default Value,Namedtuple,Optional Arguments,我试图将一个较长的空心“数据”类转换为一个命名元组。我的类当前看起来像这样: 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 =

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

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')
>>> Node()
Traceback (most recent call last):
  ...
TypeError: values not provided for field(s): val

>>> Node(3)
Node(val=3, left=None, right=None)
但这里有个问题。我的原始类允许我只传入一个值,并通过为命名/关键字参数使用默认值来处理默认值。比如:

class BinaryTree(object):
    def __init__(self, val):
        self.root = Node(val)
但这在我的重构命名元组中不起作用,因为它要求我传递所有字段。当然,我可以将
Node(val)
替换为
Node(val,None,None)
,但这不符合我的喜好


那么,有没有一个好的技巧可以让我的重写成功,而不会增加很多代码复杂性(元编程),或者我应该吞下药丸,继续进行“搜索和替换”?:)

我不确定是否有一种简单的方法只使用内置的namedtuple。有一个很好的模块名为,它具有以下功能:

>>> 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子类化,并对
\uuu new\uuu
方法进行重写:

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之前 将
节点。\uuuu新建。\uuuu默认值\uuuu
设置为默认值

>>> 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)
>>> 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 2.6之前 将
Node.\uuuuu new\uuuu.func\u默认值设置为默认值

>>> 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)
>>> 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)
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
Python2.6到3.6的包装器 这里有一个包装器,它甚至允许您(可选)将默认值设置为
None
以外的值。这不支持必需的参数

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

使用
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()自定义 原型实例:

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

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

下面是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
例如:

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

使用我的库中的
NamedTuple
类,并使用
class
语法,这非常简单:

from aenum import NamedTuple

class Node(NamedTuple):
    val = 0
    left = 1, 'previous Node', None
    right = 2, 'next Node', None
一个潜在的缺点是,对于任何具有默认值的属性,都需要使用
\uuuu doc\uuu
字符串(对于简单属性,这是可选的)。在使用中,它看起来像:

from collections import namedtuple
Node = namedtuple('Node', 'val left right')
>>> Node()
Traceback (most recent call last):
  ...
TypeError: values not provided for field(s): val

>>> Node(3)
Node(val=3, left=None, right=None)
这种方法的优点是:

是简单的,并且是基于
元类的,而不是基于
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)
>>> 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)
Test = dnamedtuple('Test', 'one two three', two=2)
Test(1, 3)  # Test(one=1, three=3, two=2)
在Python 3.6.1+中,您可以为NamedTuple字段提供默认值和类型注释。使用
键入。如果您只需要前者,请使用任何

from typing import Any, NamedTuple


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

>>> 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)
>>> 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)
Test = dnamedtuple('Test', 'one two three', two=2)
Test(1, 3)  # Test(one=1, three=3, two=2)
另外,如果您同时需要默认值和可选的可变性,Python3.7将具有在某些(许多?)情况下可以替代namedtuples的功能。
旁注:Python中当前规范(参数和变量的
之后的表达式和函数的
->
之后的表达式)的一个怪癖是在定义时对它们求值*。因此,由于“一旦类的整个主体被执行,类名就会被定义”,因此上面类字段中的
'Node'
注释必须是字符串,以避免名称错误

这种类型的提示称为“向前引用”(,),Python 3.7+将有一个
\uuuuu future\uuuuu
导入(在4.0中默认启用),它将允许使用不带引号的向前引用,从而推迟其计算


*仅AFAICT局部变量注释不会在运行时进行计算。(来源:)

在python3.7+中有一个全新的关键字参数

默认值可以是
None
或默认值的iterable。由于具有默认值的字段必须位于任何没有默认值的字段之后,因此默认值将应用于最右边的参数。例如,如果字段名是
['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)
受一个不同问题的启发,下面是我提出的基于和的解决方案(以正确处理未来的子脚本)。这与我的想法很相似

然后:


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

:

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