为啥 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
的另一个反对意见是某些位置(阅读公司)出于安全原因禁用它。
除了高级的Enum
和NamedConstant
,the aenum library* 还有基于metaclass
的NamedTuple
。
* aenum
由enum
和enum34
反向移植的作者编写。
【讨论】:
以上是关于为啥 namedtuple 模块不使用元类来创建 nt 类对象?的主要内容,如果未能解决你的问题,请参考以下文章