`super` em uma subclasse` typing.NamedTuple` falha no python 3.8

9

Eu tenho um código que funcionou no Python 3.6 e falha no Python 3.8. Parece resumir-se a chamar a supersubclasse de typing.NamedTuple, como abaixo:

<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

O objetivo desta super(object, self).__repr__chamada é usar o padrão em '<__main__.Test object at 0x7fa109953cf8>' __repr__vez de imprimir todo o conteúdo dos elementos da tupla (o que aconteceria por padrão). Existem algumas perguntas sobre como superresultar em erros semelhantes, mas eles:

  1. Consulte a versão sem parâmetros super()
  2. Já falhei no Python 3.6 (funcionou para mim antes da atualização 3.6 -> 3.8)
  3. Não consigo entender como corrigir isso de qualquer maneira, já que não é uma metaclasse personalizada sobre a qual tenho controle, mas a fornecida pelo stdlib typing.NamedTuple.

Minha pergunta é como posso corrigir isso, mantendo a compatibilidade com o Python 3.6 (caso contrário, eu usaria em @dataclasses.dataclassvez de herdar typing.NamedTuple)?

Uma questão paralela é como isso pode falhar no momento da definição, uma vez que a superchamada incorreta está dentro de um método que ainda nem foi executado. Por exemplo:

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

funciona (até chamarmos de __repr__) mesmo que fooseja uma referência indefinida. É supermágico a esse respeito?

Jatentaki
fonte
Quando executo seu código no Python 3.6, recebo uma representação do superpróprio objeto, não uma representação da sua Testinstância.
chepner
Além disso, parece que qualquer mágica em tempo de compilação que permita argumentos implícitos ainda ocorre para validar argumentos explícitos.
chepner
2
usando __repr__ = object.__repr__em sua definição de classe funciona para mim em Python3.6 e Python3.8
Azat Ibrakov
@chepner de fato, agora estou começando a ficar confuso sobre o porquê de ter funcionado antes. Mas ...
Jatentaki
Esse problema é causado pela metaclasse de typing.NamedTuple; typing.NamedTupleMeta, isso está fazendo algumas travessuras. super()requer __class__estar disponível em tempo de compilação , o que não é o caso aqui, aparentemente. Veja também: Forneça um __classcell__exemplo para a metaclasse do Python 3.6
L3viathan

Respostas:

2

Eu estava um pouco errado na outra pergunta (que acabei de atualizar). Aparentemente, esse comportamento se manifesta nos dois casos de super. Em retrospectiva, eu deveria ter testado isso.

O que está acontecendo aqui é que a metaclasse NamedTupleMetanão passa __classcell__de type.__new__fato para ela, porque cria um duplo nomeado em tempo real e o devolve. Na verdade, nas versões 3.6 e 3.7 do Python (onde ainda é um DeprecationWarning), o __classcell__vazamento no dicionário de classes, uma vez que não é removido por 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>

Usar object.__repr__diretamente como sugerido por Azat faz o truque.

como isso pode falhar no momento da definição

Da mesma maneira que o seguinte também falha:

class Foo(metaclass=1): pass

Muitas verificações são realizadas enquanto a classe está sendo construída. Entre eles, está verificando se a metaclasse passou __classcell__para o type_new.

Dimitris Fasarakis Hilliard
fonte
Isso é um bug no stdlib?
Jatentaki
@ Katentaki O vazamento de __classcell__, eu teria pensado assim. Suporte super, não sei, da mesma maneira que a herança múltipla recentemente alterada para agora gerar um erro. Ainda pode valer a pena criar um problema.
Dimitris Fasarakis Hilliard
4

Infelizmente, eu não estou tão familiarizado com o interno do CPython e a geração de classes para dizer por que ele falha, mas há esse problema do rastreador de erros do CPython que parece estar relacionado e algumas palavras nos documentos do Python

Detalhes da implementação do CPython: No CPython 3.6 e posterior, a __class__célula é passada para a metaclasse como uma __classcell__entrada no espaço de nomes da classe. Se presente, isso deve ser propagado até a type.__new__chamada para que a classe seja inicializada corretamente. Não fazer isso resultará em um RuntimeErrorno Python 3.8.

então, provavelmente em algum lugar durante a namedtuplecriação real , temos uma chamada type.__new__sem __classcell__propagação, mas não sei se é o caso.

Mas esse caso em particular parece ser solucionável ao não usar super()call dizendo explicitamente que "precisamos ter o __repr__método da objectclasse" como

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