Por que não consigo alterar o atributo __class__ de uma instância do objeto?

10
class A(object):
    pass

class B(A):
    pass

o = object()
a = A()
b = B()

Embora eu possa mudar a.__class__, não posso fazer o mesmo o.__class__(gera um TypeErrorerro). Por quê?

Por exemplo:

isinstance(a, A) # True
isinstance(a, B) # False
a.__class__ = B
isinstance(a, A) # True
isinstance(a, B) # True

isinstance(o, object) # True
isinstance(o, A) # False
o.__class__ = A # This fails and throws a TypeError
# isinstance(o, object)
# isinstance(o, A)

Sei que isso geralmente não é uma boa ideia, pois pode levar a um comportamento muito estranho se for tratado incorretamente. É apenas por uma questão de curiosidade.

Riccardo Bucco
fonte
3
Tipos internos sacrificam o dinamismo de um tipo definido pelo usuário por razões de eficiência. Observe que outra otimização opcional são os slots, que impedirão isso de forma semelhante.
precisa saber é o seguinte

Respostas:

6

O CPython tem um comentário em Objetos / typeobject.c sobre este tópico:

Nas versões do CPython anteriores à 3.5, o código compatible_for_assignmentnão foi configurado para verificar corretamente a compatibilidade de layout / slot / etc. de memória para classes não-HEAPTYPE, portanto, apenas proibimos a __class__atribuição em qualquer caso que não fosse HEAPTYPE -> HEAPTYPE.

Durante o ciclo de desenvolvimento 3.5, corrigimos o código compatible_for_assignmentpara verificar corretamente a compatibilidade entre tipos arbitrários e começamos a permitir a __class__atribuição em todos os casos em que os tipos antigo e novo tinham de fato slots e layout de memória compatíveis (independentemente de terem sido implementados como HEAPTYPEs ou não).

Porém, pouco antes do lançamento do 3.5, descobrimos que isso levava a problemas com tipos imutáveis, como int, onde o intérprete assume que eles são imutáveis ​​e usa alguns valores. Antigamente, isso não era um problema, porque eles eram realmente imutáveis ​​- em particular, todos os tipos em que o intérprete aplicava esse truque interno também eram alocados estaticamente; portanto, as regras antigas do HEAPTYPE estavam "acidentalmente" impedindo-os de permitir a __class__atribuição. Mas com as alterações na __class__atribuição, começamos a permitir códigos como

class MyInt(int):
#   ...
# Modifies the type of *all* instances of 1 in the whole program,
# including future instances (!), because the 1 object is interned.
 (1).__class__ = MyInt

(consulte https://bugs.python.org/issue24912 ).

Em teoria, a correção correta seria identificar quais classes dependem dessa invariável e de alguma forma proibir a __class__atribuição apenas para elas, talvez por meio de algum mecanismo como um novo sinalizador Py_TPFLAGS_IMMUTABLE (uma abordagem de "lista negra"). Mas, na prática, como esse problema não foi percebido no final do ciclo 3.5 RC, estamos adotando uma abordagem conservadora e restabelecendo a mesma verificação HEAPTYPE-> HEAPTYPE que costumávamos ter, além de uma "lista branca". Por enquanto, a lista de permissões consiste apenas nos subtipos ModuleType, pois esses são os casos que motivaram o patch em primeiro lugar - consulte https://bugs.python.org/issue22986 - e, como os objetos do módulo são mutáveis, podemos ter certeza que eles definitivamente não estão sendo internados. Então agora permitimos HEAPTYPE-> HEAPTYPE ou Subtipo ModuleType -> subtipo ModuleType.

Até onde sabemos, todo o código além da instrução 'if' a seguir manipulará corretamente classes não-HEAPTYPE, e a verificação HEAPTYPE é necessária apenas para proteger esse subconjunto de classes não-HEAPTYPE para as quais o intérprete cozeu na hipótese de todas as instâncias são verdadeiramente imutáveis.

Explicação:

O CPython armazena objetos de duas maneiras:

Objetos são estruturas alocadas no heap. Regras especiais se aplicam ao uso de objetos para garantir que eles sejam coletados corretamente pelo lixo. Os objetos nunca são alocados estaticamente ou na pilha; eles devem ser acessados ​​somente através de macros e funções especiais. (Objetos de tipo são exceções à primeira regra; os tipos padrão são representados por objetos de tipo inicializados estaticamente, embora o trabalho na unificação de tipo / classe para o Python 2.2 tenha possibilitado também ter objetos de tipo alocado por heap).

Informações do comentário em Incluir / objeto.h .

Quando você está tentando definir um novo valor para some_obj.__class__, a object_set_classfunção é chamada. É herdado de PyBaseObject_Type , veja o /* tp_getset */campo. Esta função verifica : o novo tipo pode substituir o tipo antigo some_obj?

Veja o seu exemplo:

class A:
    pass

class B:
    pass

o = object()
a = A() 
b = B() 

Primeiro caso:

a.__class__ = B 

O tipo de aobjeto é Ao tipo de heap, porque é alocado dinamicamente. Bem como o B. O atipo do é alterado sem problemas.

Segundo caso:

o.__class__ = B

O tipo de oé o tipo interno object( PyBaseObject_Type). Não é do tipo heap, então o TypeErroré gerado:

TypeError: __class__ assignment only supported for heap types or ModuleType subclasses.
MiniMax
fonte
4

Você só pode mudar __class__para outro tipo que tenha o mesmo layout interno (C) . O tempo de execução nem conhece esse layout, a menos que o próprio tipo seja alocado dinamicamente (um “tipo de heap”), portanto, é uma condição necessária que exclui os tipos internos como origem ou destino. Você também precisa ter o mesmo conjunto de __slots__nomes iguais.

Davis Herring
fonte