几周前我花了一些时间调查collections.namedtuple
module。该模块使用工厂函数将动态数据(新的namedtuple
类的名称和类属性名称)填充为一个非常大的字符串。然后使用字符串(表示代码)作为参数执行exec
,并返回新类。
有没有人知道为什么这样做,当有一种特定的工具可供现有的这类东西,即元类?我自己没有尝试过,但似乎namedtuple
模块中发生的一切都可以使用namedtuple
元类轻松完成,如下所示:
class namedtuple(type):
等等
issue 3974有一些提示。作者提出了一种创建命名元组的新方法,该方法被拒绝并带有以下注释:
由于对关键方法进行了硬编码,原始版本的好处似乎是速度更快。 - Antoine Pitrou
使用exec没有什么不圣洁的。早期版本使用其他方法,并且它们被证明是不必要的复杂并且具有意外问题。它是命名元组的一个关键特性,它们完全等同于手写类。 - Raymond Hettinger
另外,这里是the original namedtuple
recipe描述的一部分:
...这个配方已经发展到现在的exec风格,我们可以免费获得所有Python的高速内置参数检查。构建和执行模板的新风格使__new__和__repr__函数比此配方的先前版本更快更干净。
如果您正在寻找一些替代实现:
作为旁注:我最常反对使用exec
的另一个反对意见是,出于安全原因,某些地点(阅读公司)禁用它。
除了先进的Enum
和NamedConstant
,the aenum library *还有NamedTuple
,这是metaclass
为基础。
这是另一种方法。
""" Subclass of tuple with named fields """
from operator import itemgetter
from inspect import signature
class MetaTuple(type):
""" metaclass for NamedTuple """
def __new__(mcs, name, bases, namespace):
cls = type.__new__(mcs, name, bases, namespace)
names = signature(cls._signature).parameters.keys()
for i, key in enumerate(names):
setattr(cls, key, property(itemgetter(i)))
return cls
class NamedTuple(tuple, metaclass=MetaTuple):
""" Subclass of tuple with named fields """
@staticmethod
def _signature():
" Override in subclass "
def __new__(cls, *args):
new = super().__new__(cls, *args)
if len(new) == len(signature(cls._signature).parameters):
return new
return new._signature(*new)
if __name__ == '__main__':
class Point(NamedTuple):
" Simple test "
@staticmethod
def _signature(x, y, z): # pylint: disable=arguments-differ
" Three coordinates "
print(Point((1, 2, 4)))
如果这种方法有任何优点,那就是简单。没有NamedTuple.__new__
它会更简单,它只用于强制执行元素计数。没有它,它愉快地允许额外的匿名元素超过命名的元素,并且省略元素的主要效果是在通过名称访问时省略的元素上的IndexError
(有一些可以转换为AttributeError
的工作)。错误的元素计数的错误消息有点奇怪,但它得到了重点。我不希望这与Python 2一起使用。
还有进一步复杂化的空间,例如__repr__
方法。我不知道性能如何与其他实现相比(缓存签名长度可能有帮助),但我更喜欢调用约定与本机namedtuple
实现相比。
另一个原因是其他答案都没有达到*。
一个类只能有1个元类。其中一个原因是元类充当创建类的工厂。威利不可能将工厂混合在一起。您必须创建一个“组合工厂”,它知道如何以正确的顺序调用多个工厂,或者创建一个知道“父工厂”的“子工厂”,并正确使用它。
如果namedtuple
使用自己的元类,那么涉及任何其他元类的继承将会破坏:
>>> class M1(type): ...
...
>>> class M2(type): ...
...
>>> class C1(metaclass=M1): ...
...
>>> class C2(metaclass=M2): ...
...
>>> class C(C1, C2): ...
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
相反,如果你想拥有自己的元类并继承自namedtuple
类,你必须使用某种所谓的namedtuple_meta
元类来做到这一点:
from namedtuple import namedtuple_meta # pretending this exists
class MyMeta(type): ...
class MyMetaWithNT(namedtuple_meta, MyMeta): ...
class C(metaclass=MyMetaWithNT): ...
..或者直接从namedtuple_meta
继承自定义元类:
class MyMeta(namedtuple_meta): ...
class C(metaclass=MyMeta): ...
这看起来很简单,但编写自己的mataclass可以很快地解决一些(复杂的)nt元类问题。这种限制可能不会经常出现,但往往足以阻碍namedtuple
的使用。因此,将所有namedtuple
类都设置为type
类型并消除自定义元类的复杂性绝对是一个优势。
* Raymond Hettinger的comment暗示了它:
它是命名元组的一个关键特性,它们完全等同于手写类。