`typing.NamedTuple` 子类中的 `super` 在 python 3.8 中失败

`super` in a `typing.NamedTuple` subclass fails in python 3.8

我的代码在 Python 3.6 中有效,但在 Python 3.8 中失败。它似乎归结为在 typing.NamedTuple 的子类中调用 super,如下所示:

<ipython-input-2-fea20b0178f3> in <module>
----> 1 class Test(typing.NamedTuple):
      2     a: int
      3     b: float
      4     def __repr__(self):
      5         return super(object, self).__repr__()

RuntimeError: __class__ not set defining 'Test' as <class '__main__.Test'>. Was __classcell__ propagated to type.__new__?
In [3]: class Test(typing.NamedTuple): 
   ...:     a: int 
   ...:     b: float 
   ...:     #def __repr__(self): 
   ...:     #    return super(object, self).__repr__() 
   ...:                                                                         

>>> # works

super(object, self).__repr__ 调用的目的是使用标准 '<__main__.Test object at 0x7fa109953cf8>' __repr__ 而不是打印出元组元素的所有内容(默认情况下会发生)。 super 上有 some 导致类似的错误,但它们:

  1. 参考无参数版本super()
  2. 在 Python 3.6 中已经失败(在 3.6 -> 3.8 升级之前对我有用)
  3. 我还是不明白如何解决这个问题,因为它不是我可以控制的自定义元类,而是 stdlib 提供的 typing.NamedTuple

我的问题是如何在保持与 Python 3.6 的向后兼容性的同时解决此问题(否则我只使用 @dataclasses.dataclass 而不是从 typing.NamedTuple 继承)?

一个附带的问题是,如果有问题的 super 调用在一个甚至尚未执行的方法中,这怎么会在定义时 失败。例如:

In [3]: class Test(typing.NamedTuple): 
   ...:     a: int 
   ...:     b: float 
   ...:     def __repr__(self): 
   ...:         return foo 

有效(直到我们实际调用 __repr__),即使 foo 是一个未定义的引用。 super 在这方面很神奇吗?

不幸的是,我对 CPython 的内部结构和 类 代不太熟悉,无法解释它失败的原因,但是有 this CPython bug tracker issue which seems to be related and some words in Python docs

CPython implementation detail: In CPython 3.6 and later, the __class__ cell is passed to the metaclass as a __classcell__ entry in the class namespace. If present, this must be propagated up to the type.__new__ call in order for the class to be initialised correctly. Failing to do so will result in a RuntimeError in Python 3.8.

所以可能在实际 namedtuple 创建过程中的某个地方,我们调用了 type.__new__ 而没有 __classcell__ 传播,但我不知道是不是这样。

但这种特殊情况似乎可以通过不使用 super() 调用来解决,并明确说明 "we need to have __repr__ method of the object class" 喜欢

class Test(typing.NamedTuple):
    a: int
    b: float
    __repr__ = object.__repr__

我在 (我刚刚更新)中有点错误。 显然,这种行为在 super 两个 案例中都有体现。事后看来,我应该测试一下。

这里发生的是 metaclass NamedTupleMeta 确实没有将 __classcell__ 传递给 type.__new__ 因为它动态创建了一个命名元组 returns那个。实际上,在 Python 的 3.6 和 3.7 中(这仍然是 DeprecationWarning),__classcell__ 泄漏到 class 字典中,因为它没有被 NamedTupleMeta.__new__.

class Test(NamedTuple):
    a: int
    b: float
    def __repr__(self):
        return super().__repr__()

# isn't removed by NamedTupleMeta
Test.__classcell__
<cell at 0x7f956562f618: type object at 0x5629b8a2a708>

按照 Azat 的建议直接使用 object.__repr__ 即可。

how can this fail at definition time

以下同样失败:

class Foo(metaclass=1): pass

在构建 class 时会执行许多 检查。其中,正在检查 metaclass 是否已将 __classcell__ 传递给 type_new

以下:https://bugs.python.org/issue42765?

这个技巧(不使用 super())救了我: return 元组.getitem(self, index_or_key)

另一种似乎至少对我有用的解决方法如下:

class BaseTest(typing.NamedTuple):
    a: int
    b: float
    

class Test(BaseTest):
    def __repr__(self):
        return super().__repr__()

通过子class基于 NameTuple 的 class,似乎 class 单元格被删除了。