【问题标题】: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):

等等等等

【问题讨论】:

    标签: python metaclass python-internals namedtuple


    【解决方案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__ 的功能比本秘籍的先前版本更快、更简洁。

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

        【讨论】:

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

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

        除了高级的EnumNamedConstantthe aenum library* 还有基于metaclassNamedTuple


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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-07-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多