为什么 namedtuple 模块不使用 metaclass 来创建 ntclass 对象?

Why doesn't the namedtuple module use a metaclass to create nt class objects?

几周前我花了一些时间调查collections.namedtuple module。该模块使用工厂函数将动态数据(新 namedtuple class 的名称和 class 属性名称)填充到一个非常大的字符串中。然后以字符串(代表代码)为参数执行exec,返回新的class。

有没有人知道为什么要这样做,当有一个特定的工具可以轻松获得这种东西时,即 metaclass?我自己没有尝试这样做,但似乎 namedtuple 模块中发生的一切都可以使用 namedtuple metaclass 轻松完成,如下所示:

class namedtuple(type):

等等等等

issue 3974中有一些提示。作者提出了一种创建命名元组的新方法,被拒绝,评论如下:

It seems the benefit of the original version is that it's faster, thanks to hardcoding critical methods. - Antoine Pitrou

There is nothing unholy about using exec. Earlier versions used other approaches and they proved unnecessarily complex and had unexpected problems. It is a key feature for named tuples that they are exactly equivalent to a hand-written class. - Raymond Hettinger

另外,这里是the original namedtuple recipe的部分描述:

... the recipe has evolved to its current exec-style where we get all of Python's high-speed builtin argument checking for free. The new style of building and exec-ing a template made both the __new__ and __repr__ functions faster and cleaner than in previous versions of this recipe.

如果您正在寻找一些替代实现:

作为旁注:我最常看到的反对使用 exec 的另一个反对意见是,出于安全原因,某些位置(阅读公司)禁用它。

除了高级的 EnumNamedConstantthe aenum library* 还有 NamedTuple,这是基于 metaclass 的。


* aenumenum and the enum34 backport 的作者编写。

这是另一种方法。

""" 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 在按名称访问省略元素时对省略的元素进行 IndexError (可以翻译成 AttributeError).元素计数不正确的错误消息有点奇怪,但它明白了要点。我不希望这适用于 Python 2.

还有进一步复杂化的空间,例如 __repr__ 方法。我不知道性能与其他实现相比如何(缓存签名长度可能会有所帮助),但与本机 namedtuple 实现相比,我更喜欢调用约定。

经过多年的经验回到这个问题:以下是 none 其他答案突然想到的其他几个原因*。

每个 class 只允许 1 个元class

一个class只能有1个metaclass。 metaclass 充当创建 class 的工厂,并且不可能随意将工厂混合在一起。您必须创建一个知道如何以正确顺序调用多个工厂的“组合工厂”,或者一个知道“父工厂”并正确使用它的“子工厂”。

如果 namedtuple 使用自己的元class,涉及任何其他元class 的继承将中断:

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

相反,如果您想拥有自己的元class 并继承自 namedtuple class,则必须使用某种 so-called namedtuple_meta metaclass 来做到这一点:

from namedtuple import namedtuple_meta  # pretending this exists

class MyMeta(type): ...

class MyMetaWithNT(namedtuple_meta, MyMeta): ...

class C(metaclass=MyMetaWithNT): ...

..或者直接从namedtuple_meta继承自定义元class:

class MyMeta(namedtuple_meta): ...

class C(metaclass=MyMeta): ...

乍一看这看起来很简单,但是编写自己的 metaclass 并与一些(复杂的)nt metaclass 配合得很好可能很快就会出现问题。这个限制可能不会经常出现,但经常会阻碍 namedtuple 的使用。因此,让所有 namedtuple class 都是 type 类型并消除自定义元 class.

的复杂性绝对是一个优势

元class,还是元编程?

“为什么不使用元class?!?”这个问题忽略了一个基本问题。是:nt的用途是什么?

目的不仅仅是创建一个 class 工厂。如果是这样,metaclass 就完美了。 namedtuple 的真正目的不仅仅是最终功能,而是自动生成一个 class 结构,代码在各个方面都简单易懂,就好像它是由经验丰富的专业人士手写的一样。这需要元编程——自动生成不是class,而是代码 ].这是两个不同的东西。它与较新的 dataclasses 模块非常相似,后者为您编写方法(而不是编写整个 class,如 namedtuple)。


* Raymond Hettinger 的 comment 确实暗示了这一点:

It is a key feature for named tuples that they are exactly equivalent to a hand-written class.