Warum kann ich das Attribut __class__ einer Objektinstanz nicht ändern?

10
class A(object):
    pass

class B(A):
    pass

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

Ich kann mich zwar ändern a.__class__, aber ich kann nicht dasselbe tun o.__class__(es wird ein TypeErrorFehler ausgegeben). Warum?

Zum Beispiel:

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)

Ich weiß, dass dies im Allgemeinen keine gute Idee ist, da es zu einem sehr seltsamen Verhalten führen kann, wenn es falsch behandelt wird. Es ist nur aus Neugier.

Riccardo Bucco
quelle
3
Eingebaute Typen opfern aus Effizienzgründen die Dynamik eines benutzerdefinierten Typs. Beachten Sie, dass eine weitere optionale Optimierung Slots sind, die dies ebenfalls verhindern.
juanpa.arrivillaga

Antworten:

6

CPython hat in Objects / typeobject.c einen Kommentar zu diesem Thema:

In Versionen von CPython vor 3.5 wurde der Code in compatible_for_assignmentnicht so eingerichtet, dass die Kompatibilität von Speicherlayout / Steckplatz / usw. für Nicht-HEAPTYPE-Klassen korrekt überprüft wurde. Daher haben wir die __class__Zuweisung in keinem Fall zugelassen, der nicht HEAPTYPE -> HEAPTYPE war.

Während des Entwicklungszyklus 3.5 haben wir den Code compatible_for_assignmentkorrigiert, um die Kompatibilität zwischen beliebigen Typen korrekt zu überprüfen, und die __class__Zuweisung in allen Fällen zugelassen, in denen der alte und der neue Typ tatsächlich kompatible Slots und Speicherlayouts hatten (unabhängig davon, ob sie als HEAPTYPEs implementiert wurden) oder nicht).

Kurz vor der Veröffentlichung von 3.5 stellten wir jedoch fest, dass dies zu Problemen mit unveränderlichen Typen wie int führte, bei denen der Interpreter davon ausgeht, dass sie unveränderlich sind, und einige Werte interniert. Früher war dies kein Problem, da sie wirklich unveränderlich waren - insbesondere wurden alle Typen, bei denen der Interpreter diesen Internierungstrick anwendete, auch statisch zugewiesen, sodass die alten HEAPTYPE-Regeln "versehentlich" verhinderten, dass sie zugewiesen werden konnten __class__. Aber mit den Änderungen an der __class__Zuweisung haben wir begonnen, Code wie zuzulassen

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

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

Theoretisch wäre die richtige Lösung, zu identifizieren, welche Klassen sich auf diese Invariante stützen, und die __class__Zuweisung nur für sie irgendwie zu verbieten , möglicherweise über einen Mechanismus wie ein neues Py_TPFLAGS_IMMUTABLE-Flag (ein "Blacklisting" -Ansatz). In der Praxis verfolgen wir jedoch den konservativen Ansatz und setzen den gleichen HEAPTYPE-> HEAPTYPE-Check wie früher sowie eine "Whitelist" wieder ein, da dieses Problem erst spät im 3.5 RC-Zyklus bemerkt wurde. Derzeit besteht die Whitelist nur aus ModuleType-Subtypen, da dies die Fälle waren, die den Patch überhaupt motiviert haben - siehe https://bugs.python.org/issue22986 - und da Modulobjekte veränderbar sind, können wir sicher sein dass sie definitiv nicht interniert werden. Also erlauben wir jetzt HEAPTYPE-> HEAPTYPE oder Subtyp ModuleType -> Subtyp ModuleType.

Soweit wir wissen, wird der gesamte Code, der über die folgende 'if'-Anweisung hinausgeht, Nicht-HEAPTYPE-Klassen korrekt verarbeiten, und die HEAPTYPE-Prüfung wird nur benötigt, um die Teilmenge der Nicht-HEAPTYPE-Klassen zu schützen, für die der Interpreter unter der Annahme gebacken hat, dass Alle Instanzen sind wirklich unveränderlich.

Erläuterung:

CPython speichert Objekte auf zwei Arten:

Objekte sind Strukturen, die auf dem Heap zugeordnet sind. Für die Verwendung von Objekten gelten besondere Regeln, um sicherzustellen, dass sie ordnungsgemäß gesammelt werden. Objekte werden niemals statisch oder auf dem Stapel zugewiesen. Auf sie darf nur über spezielle Makros und Funktionen zugegriffen werden. (Typobjekte sind Ausnahmen von der ersten Regel. Die Standardtypen werden durch statisch initialisierte Typobjekte dargestellt, obwohl die Arbeit an der Typ- / Klassenvereinigung für Python 2.2 auch die Zuordnung von Typobjekten auf dem Heap ermöglichte.)

Informationen aus dem Kommentar in Include / object.h .

Wenn Sie versuchen, einen neuen Wert auf festzulegen, some_obj.__class__wird die object_set_classFunktion aufgerufen. Es wird von PyBaseObject_Type geerbt , siehe /* tp_getset */Feld. Diese Funktion prüft : Kann der neue Typ den alten Typ ersetzen some_obj?

Nehmen Sie Ihr Beispiel:

class A:
    pass

class B:
    pass

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

Erster Fall:

a.__class__ = B 

Der aObjekttyp ist Ader Heap-Typ, da er dynamisch zugewiesen wird. Sowie die B. Der aTyp wird problemlos geändert.

Zweiter Fall:

o.__class__ = B

Der Typ von oist der integrierte Typ object( PyBaseObject_Type). Es ist kein Heap-Typ, daher TypeErrorwird das ausgelöst:

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

Sie können nur __class__zu einem anderen Typ wechseln , der dasselbe interne (C) Layout hat . Die Laufzeit kennt dieses Layout nur, wenn der Typ selbst dynamisch zugewiesen wird (ein „Heap-Typ“). Dies ist also eine notwendige Bedingung, die die integrierten Typen als Quelle oder Ziel ausschließt. Sie müssen auch den gleichen Satz __slots__mit den gleichen Namen haben.

Davis Herring
quelle