Perché non posso modificare l'attributo __class__ di un'istanza di oggetto?


10
class A(object):
    pass

class B(A):
    pass

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

Mentre posso cambiare a.__class__, non posso fare lo stesso con o.__class__(genera un TypeErrorerrore). Perché?

Per esempio:

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)

So che in genere questa non è una buona idea, dal momento che può comportare un comportamento molto strano se viene gestita in modo errato. È solo per motivi di curiosità.


3
I tipi incorporati sacrificano il dinamismo di un tipo definito dall'utente per motivi di efficienza. Nota, un'altra ottimizzazione opzionale sono gli slot, che impediranno allo stesso modo questo.
juanpa.arrivillaga,

Risposte:


6

CPython ha un commento in Oggetti / typeobject.c su questo argomento:

Nelle versioni di CPython precedenti alla 3.5, il codice in compatible_for_assignmentnon era impostato per verificare correttamente la compatibilità del layout di memoria / slot / ecc. Per le classi non HEAPTYPE, quindi abbiamo semplicemente vietato l' __class__assegnazione in ogni caso che non fosse HEAPTYPE -> HEAPTYPE.

Durante il ciclo di sviluppo 3.5, abbiamo corretto il codice compatible_for_assignmentper verificare correttamente la compatibilità tra tipi arbitrari e abbiamo iniziato a consentire l' __class__assegnazione in tutti i casi in cui i tipi vecchi e nuovi avevano effettivamente slot e layout di memoria compatibili (indipendentemente dal fatto che fossero implementati come HEAPTYPEs o no).

Poco prima del rilascio di 3.5, tuttavia, abbiamo scoperto che questo portava a problemi con tipi immutabili come int, in cui l'interprete assume che siano immutabili e integra alcuni valori. In precedenza questo non era un problema, perché erano davvero immutabili - in particolare, tutti i tipi in cui l'interprete applicava questo trucco di internamento erano stati allocati staticamente, quindi le vecchie regole di HEAPTYPE impedivano "accidentalmente" di consentire l' __class__assegnazione. Ma con le modifiche __class__all'assegnazione, abbiamo iniziato a consentire codice simile

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

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

In teoria la soluzione corretta sarebbe quella di identificare quali classi si basano su questo invariante e in qualche modo non consentire l' __class__assegnazione solo per loro, forse attraverso un meccanismo come un nuovo flag Py_TPFLAGS_IMMUTABLE (un approccio di "lista nera"). Ma in pratica, dal momento che questo problema non è stato notato alla fine del ciclo 3.5 RC, stiamo adottando l'approccio conservativo e ripristinando lo stesso controllo HEAPTYPE-> HEAPTYPE che avevamo prima, oltre a una "lista bianca". Per ora, la whitelist è composta solo dai sottotipi ModuleType, dal momento che questi sono i casi che hanno motivato la patch in primo luogo - vedi https://bugs.python.org/issue22986 - e poiché gli oggetti del modulo sono mutabili possiamo essere sicuri che sicuramente non vengono internati. Quindi ora consentiamo HEAPTYPE-> HEAPTYPE o Sottotipo ModuleType -> Sottotipo ModuleType.

Per quanto ne sappiamo, tutto il codice oltre la seguente istruzione 'if' gestirà correttamente le classi non HEAPTYPE e il controllo HEAPTYPE è necessario solo per proteggere quel sottoinsieme di classi non HEAPTYPE per il quale l'interprete ha elaborato supponendo che tutte le istanze sono veramente immutabili.

Spiegazione:

CPython memorizza gli oggetti in due modi:

Gli oggetti sono strutture allocate sull'heap. Regole speciali si applicano all'uso di oggetti per garantire che siano adeguatamente raccolti. Gli oggetti non vengono mai allocati staticamente o in pila; è necessario accedervi solo tramite macro e funzioni speciali. (Gli oggetti di tipo sono eccezioni alla prima regola; i tipi standard sono rappresentati da oggetti di tipo inizializzati staticamente, sebbene il lavoro sull'unificazione di tipo / classe per Python 2.2 abbia reso possibile avere anche oggetti di tipo allocati in heap).

Informazioni tratte dal commento in Include / object.h .

Quando si tenta di impostare un nuovo valore su some_obj.__class__, object_set_classviene chiamata la funzione. È ereditato da PyBaseObject_Type , vedi /* tp_getset */campo. Questa funzione verifica : il nuovo tipo può sostituire il vecchio tipo in some_obj?

Prendi il tuo esempio:

class A:
    pass

class B:
    pass

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

Primo caso:

a.__class__ = B 

Il tipo di aoggetto è A, il tipo di heap, perché è allocato in modo dinamico. Così come il B. Il atipo di è cambiato senza problemi.

Secondo caso:

o.__class__ = B

Il tipo di oè il tipo incorporato object( PyBaseObject_Type). Non è un tipo di heap, quindi TypeErrorviene sollevato:

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

4

Puoi solo passare __class__a un altro tipo con lo stesso layout interno (C) . Il runtime non conosce nemmeno quel layout a meno che il tipo stesso non sia allocato dinamicamente (un "tipo di heap"), quindi questa è una condizione necessaria che esclude i tipi integrati come origine o destinazione. Devi anche avere lo stesso set di __slots__con gli stessi nomi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.