为啥 namedtuple 模块不使用元类来创建 nt 类对象?

Posted

技术标签:

【中文标题】为啥 namedtuple 模块不使用元类来创建 nt 类对象?【英文标题】:Why doesn't the namedtuple module use a metaclass to create nt class objects?为什么 namedtuple 模块不使用元类来创建 nt 类对象? 【发布时间】:2015-03-26 21:31:56 【问题描述】:

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

有谁知道为什么要这样做,当有一种现成的用于这种东西的特定工具时,即元类?我自己没有尝试过,但似乎namedtuple 模块中发生的所有事情都可以使用namedtuple 元类轻松完成,如下所示:

class namedtuple(type):

等等等等

【问题讨论】:

【参考方案1】:

在多年的经验之后回到这个问题:以下是其他几个答案都没有遇到的其他原因*。

每个类只允许 1 个元类

一个类只能有 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): ...

一开始这看起来很容易,但是编写自己的元类来与一些(复杂的)nt 元类很好地配合可能很快就会出现问题。这种限制可能不会经常出现,但经常会阻碍namedtuple 的使用。因此,让所有namedtuple 类都为type 类型绝对是一个优势,并消除了自定义元类的复杂性。

元类,还是元编程?

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

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


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

命名元组的一个关键特性是它们完全等同于手写类。

【讨论】:

【参考方案2】:

这是另一种方法。

""" 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 实现相比,我更喜欢调用约定。

【讨论】:

【参考方案3】:

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

看来原版的好处是速度更快, 感谢硬编码关键方法。 - 安托万·皮特鲁

使用 exec 并没有什么不好。早期版本使用其他 方法,它们被证明是不必要的复杂,并有意想不到的 问题。命名元组的一个关键特性是它们完全是 相当于手写课。 - Raymond Hettinger

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

...配方已经演变为当前的 exec 风格,我们得到了所有 Python 的高速内置参数检查免费。新的 构建和执行模板的风格使得 __new__ 和 __repr__ 的功能比本秘籍的先前版本更快、更简洁。

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

abstract base class + mix-in for named tuples Jan Kaliszewski 的食谱

metaclass-based implementation by Aaron Iles(参见他的blog post)

【讨论】:

嗯。这肯定回答了一般问题,但我很想知道这些意想不到的问题在哪里。根据它们是什么,问题可能出在元类本身上,在这种情况下,它们可能应该被修复。似乎 2.5 年后对该评论的回复也提出了一些人可能遇到的一些实际问题。无论如何,感谢您的链接-那里有很多信息。 我从来没有真正买过这个。在我看来,答案总是“因为 Raymond Hettinger 可以使用奇怪的技巧”。 作为一个正在学习的人,在标准库中看到类似的东西真的让我停下来。我曾假设标准库将是检查“好代码”应该是什么样子的好地方。但是正如上面的评论者所说,以这种方式使用exec 似乎是一种黑客行为,这有点令人失望。元类非常棒,但如果标准库本身避免在如此明显的情况下使用它们,那么拥有它们有什么意义呢? 是的,为速度而生,这可能是另一个 标准库不是寻找“好代码”的地方——尤其是随着语言的发展。更新 stdlib 以遵循新实践或利用新模块的机会很多,会引入新的错误,因此很少这样做。【参考方案4】:

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

除了高级的EnumNamedConstant,the aenum library* 还有基于metaclassNamedTuple


* aenumenumenum34 反向移植的作者编写。

【讨论】:

以上是关于为啥 namedtuple 模块不使用元类来创建 nt 类对象?的主要内容,如果未能解决你的问题,请参考以下文章

我可以使用 python 元类来跟踪单独文件中的子类吗?

Python 元类

Python利用元类来控制实例创建

使用元类动态设置属性

如何在 python 中使用元类来增加或覆盖添加到类中的方法

2.自定义元类控制类的创建行为