`super` в подклассе `typing.NamedTuple` не работает в python 3.8

У меня есть код, который работал в Python 3.6 и не работает в Python 3.8. Кажется, все сводится к вызову super в подклассе typing.NamedTuple, как показано ниже:

<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 приводит к аналогичным ошибкам, но они:

  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 волшебным в этом отношении?


person Jatentaki    schedule 01.05.2020    source источник
comment
Когда я запускаю ваш код в Python 3.6, я получаю представление самого объекта super, а не представление вашего экземпляра Test.   -  person chepner    schedule 01.05.2020
comment
Кроме того, похоже, что любая магия времени компиляции, позволяющая использовать неявные аргументы, по-прежнему используется для некоторой проверки явных аргументов.   -  person chepner    schedule 01.05.2020
comment
использование __repr__ = object.__repr__ в вашем определении класса работает для меня на Python3.6 и Python3.8   -  person Azat Ibrakov    schedule 01.05.2020
comment
@chepner действительно, теперь я начинаю путаться в том, почему это работало раньше. Но получилось...   -  person Jatentaki    schedule 01.05.2020
comment
Эта проблема вызвана метаклассом typing.NamedTuple; typing.NamedTupleMeta, это какие-то махинации. super() требует, чтобы __class__ был доступен во время компиляции, что, по-видимому, здесь не так. См. также: Приведите __classcell__ пример для метакласса Python 3.6   -  person L3viathan    schedule 01.05.2020
comment
@AzatIbrakov Спасибо, это хорошая идея. Пожалуйста, продолжайте публиковать это как ответ, если никто не ответит на это более подробно в ближайшее время, я приму это.   -  person Jatentaki    schedule 01.05.2020
comment
Я поддерживаю предложение Азата: поскольку object обязательно является последним классом в MRO любого класса, вам не нужны кооперативные способности super.   -  person chepner    schedule 01.05.2020
comment
NamedTuple имеет метакласс, ограничивающий магию перегрузки методы.   -  person pylang    schedule 04.05.2020


Ответы (2)


Я немного ошибся в другом вопросе (который я только что обновил). Судя по всему, такое поведение проявляется в обоих случаях super. Оглядываясь назад, я должен был проверить это.

Здесь происходит то, что метакласс NamedTupleMeta действительно не передает __classcell__ type.__new__, потому что он создает именованный кортеж на лету и возвращает его. На самом деле, в Python 3.6 и 3.7 (где это все еще DeprecationWarning) __classcell__ просачивается в словарь классов, так как он не удаляется 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>

Использование object.__repr__ напрямую, как предложил Азат, помогает.

как это может потерпеть неудачу во время определения

Точно так же следующее также терпит неудачу:

class Foo(metaclass=1): pass

Многие проверки выполняются во время создания класса. Среди них проверка того, передал ли метакласс __classcell__ type_new.

person Dimitris Fasarakis Hilliard    schedule 01.05.2020
comment
Это ошибка в stdlib тогда? - person Jatentaki; 01.05.2020
comment
@Jatentaki Утечка __classcell__, я бы так и подумал. Поддержка super, не знаю, так же, как множественное наследование недавно изменено на повышение ошибка. Тем не менее, возможно, стоит создать проблему. - person Dimitris Fasarakis Hilliard; 01.05.2020

К сожалению, я не настолько знаком с внутренним устройством CPython и генерацией классов, чтобы сказать, почему это не удается, но есть эта ошибка CPython проблема с трекером, которая кажется связанной, и несколько слов в Документация по Python

Детали реализации CPython: в CPython 3.6 и более поздних версиях ячейка __class__ передается в метакласс как запись __classcell__ в пространстве имен класса. Если он присутствует, он должен распространяться до вызова type.__new__, чтобы класс был правильно инициализирован. Невыполнение этого требования приведет к RuntimeError в Python 3.8.

поэтому, вероятно, где-то во время фактического создания namedtuple у нас есть вызов type.__new__ без распространения __classcell__, но я не знаю, так ли это.

Но этот конкретный случай кажется разрешимым, если не использовать вызов super() с явным указанием, что «нам нужен метод __repr__ класса object», например

class Test(typing.NamedTuple):
    a: int
    b: float
    __repr__ = object.__repr__
person Azat Ibrakov    schedule 01.05.2020