有什么办法可以将工厂函数赋予“typing.NamedTuple”子类吗?

Is there any way to give factory function(s) to `typing.NamedTuple` subclasses?

注意:我在 Python 3.6.7 中完成了此操作,但是 NamedTuple 中的文档没有更改,所以我怀疑它是否会更改。

所以我正在查看 typing 包中的 NamedTuple class,我想知道是否有办法向其添加可变默认值。第一次尝试是看看我是否可以使用 _make class 方法对我有利,但后来我发现 class 检查是否覆盖 __new__.

(回顾:如果将 [] 放入默认值,最终每个对象共享同一个列表。旧的 collections.namedtuple 和新的 [=19] 都是如此=]。这就是为什么 collections.defaultdict class 在构造函数中有一个 default_factory 参数。)

>>> from typing import NamedTuple, List
>>> class Person(NamedTuple):
...   name: str
...   children: List['Person']
...   def __new__(self, name: str, children: List['Person'] = None):
...     return Person._make(name, children if children is not None else [])
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.6/typing.py", line 2163, in __new__
    raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
AttributeError: Cannot overwrite NamedTuple attribute __new__
>>>

我继续对如何将列表偷偷塞进顽固的 __new__ 感到困惑,但后来它开始变得普遍没有意义。

>>> class Person(NamedTuple):
...   name: str
...   children: List['Person'] = None
... 
>>> 
>>> def new_new(name: str, children: List[Person] = None) -> Person:
...   return Person(name, [] if children is None else children)
... 
>>> old_new = Person.__new__
>>> def new_new(name: str, children: List[Person] = None) -> Person:
...   return old_new(name, [] if children is None else children)
... 
>>> Person.__new__ = new_new
>>> 
>>> Person('John')
Person(name='John', children=None)
>>> Person.__new__
<function new_new at 0x7f776b2b2e18>
>>> new_new('John')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in new_new
  File "<string>", line 14, in __new__
TypeError: tuple.__new__(X): X is not a type object (str)
>>> new_new(Person, 'John')
Person(name='John', children=None)
>>> 

那么,应该怎么做呢?我习惯为 collections.namedtuple classes 做同样的事情,这主要是出于好奇。

实际上,我应该这样做:

class Person(NamedTuple):
  name: str
  children: List['Person'] = None

  @classmethod
  def create(cls, name, children=None):
    return cls(name, [] if children is None else children)

NamedTuple 的目的不只是提供经过类型检查的命名元组。 NamedTupleMeta explicitly prohibits 覆盖 __new____init__ 和其他一些。您唯一的机会是改变 NamedTuple 的工作方式。


防止覆盖 __new__ 本身不受保护。这允许您以各种方式对其进行猴子修补。


您可以从受保护的名称中删除 __new__

import typing
typing._prohibited = typing._prohibited[1:]

这允许您直接覆盖 class 中的 __new__。 请注意,这将影响 所有 NamedTuple 子类型。


您可以派生一个不保护 __new__ 的新元class,并将其用于您的 NamedTuple 实例:

class NamedTupleUnprotectedMeta(typing.NamedTupleMeta):
    def __new__(cls, typename, bases, ns):
        ...
        # copy verbatim from NamedTupleMeta
        ...
        # update from user namespace without protection
        for key in ns:
            if key not in typing._special and key not in nm_tpl._fields:
                setattr(nm_tpl, key, ns[key])
        return nm_tpl


class Person(NamedTuple, metaclass=NamedTupleUnprotectedMeta):
    ...

这允许您直接覆盖 class 中的 __new__,而不影响 NamedTuple 的其他子类型。请注意,您还可以让您的 metaclass 添加自定义 new 调用 class 正文中设置的默认值。


注意 NamedTupleMeta 也会忽略 bases,所以你也不能使用 mixins 之类的。任何广泛的更改都需要先重写 NamedTupleMeta


如果您只需要一个带有默认值的存储-class,dataclasses standard library package and the attrs third-party package 会提供与您描述的相匹配的声明:

@attr.s(auto_attribs=True)
class Person:
    name: str
    children: List['Person'] = attr.Factory(list)

两个包都支持冻结,类似于元组的不变性