Dlaczego nie mogę zmienić atrybutu __class__ instancji obiektu?

10
class A(object):
    pass

class B(A):
    pass

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

Chociaż mogę się zmienić a.__class__, nie mogę zrobić tego samego z o.__class__(generuje TypeErrorbłąd). Dlaczego?

Na przykład:

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)

Wiem, że na ogół nie jest to dobry pomysł, ponieważ może prowadzić do bardzo dziwnych zachowań, jeśli zostanie źle potraktowany. To tylko ze względu na ciekawość.

Riccardo Bucco
źródło
3
Typy wbudowane poświęcają dynamikę typu zdefiniowanego przez użytkownika ze względu na wydajność. Uwaga: inną opcjonalną optymalizacją są gniazda, które podobnie temu zapobiegną.
juanpa.arrivillaga

Odpowiedzi:

6

CPython ma komentarz w Objects / typeobject.c na ten temat:

W wersjach CPython wcześniejszych niż 3.5 kod w compatible_for_assignmentnie został skonfigurowany do prawidłowego sprawdzania zgodności układu / gniazda pamięci itp. Dla klas innych niż HEAPTYPE, więc po prostu zabroniliśmy __class__przypisywania w każdym przypadku, który nie był HEAPTYPE -> HEAPTYPE.

Podczas cyklu programowania 3.5 naprawiliśmy kod, compatible_for_assignmentaby poprawnie sprawdzać zgodność między dowolnymi typami, i zaczęliśmy zezwalać na __class__przypisywanie we wszystkich przypadkach, w których stare i nowe typy faktycznie miały zgodne gniazda i układ pamięci (niezależnie od tego, czy zostały zaimplementowane jako HEAPTYPE albo nie).

Jednak tuż przed wydaniem 3.5 odkryliśmy, że doprowadziło to do problemów z niezmiennymi typami, takimi jak int, gdzie interpreter zakłada, że ​​są niezmienne i internalizuje niektóre wartości. Dawniej nie był to problem, ponieważ były one naprawdę niezmienne - w szczególności wszystkie typy, w których interpreter stosował tę sztuczkę internowania, również były przydzielane statycznie, więc stare reguły HEAPTYPE „przypadkowo” uniemożliwiały im __class__przypisanie. Ale wraz ze zmianami __class__przypisania zaczęliśmy zezwalać na kod podobny

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

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

Teoretycznie poprawną poprawką byłoby określenie, które klasy polegają na tym niezmienniku i w jakiś sposób zabraniają __class__przypisywania tylko dla nich, być może za pomocą jakiegoś mechanizmu, takiego jak nowa flaga Py_TPFLAGS_IMMUTABLE (podejście „czarnej listy”). Ale w praktyce, ponieważ problem ten nie został zauważony w późnej fazie 3.5 RC, zachowujemy ostrożne podejście i przywracamy to samo sprawdzenie HEAPTYPE-> HEAPTYPE, które mieliśmy wcześniej, oraz „białą listę”. Na razie biała lista składa się tylko z podtypów ModuleType, ponieważ są to przypadki, które przede wszystkim motywowały łatkę - patrz https://bugs.python.org/issue22986 - a ponieważ obiekty modułów są zmienne, możemy być pewni że zdecydowanie nie są internowani. Teraz pozwalamy HEAPTYPE-> HEAPTYPE lub Podtyp ModuleType -> Podtyp ModuleType.

O ile wiemy, cały kod poza poniższą instrukcją „if” będzie poprawnie obsługiwał klasy inne niż HEAPTYPE, a kontrola HEAPTYPE jest potrzebna tylko w celu ochrony tego podzbioru klas innych niż HEAPTYPE, dla których interpreter przygotował założenie, że wszystkie instancje są naprawdę niezmienne.

Wyjaśnienie:

CPython przechowuje obiekty na dwa sposoby:

Obiekty są strukturami przydzielonymi na stercie. Specjalne zasady mają zastosowanie do korzystania z obiektów, aby zapewnić, że są one odpowiednio odśmiecane. Obiekty nigdy nie są przydzielane statycznie ani na stosie; dostęp do nich można uzyskać tylko poprzez specjalne makra i funkcje. (Obiekty typu są wyjątkami od pierwszej reguły; typy standardowe są reprezentowane przez statycznie inicjowane obiekty typu, chociaż praca nad ujednoliceniem typu / klasy dla Pythona 2.2 umożliwiła również posiadanie obiektów typu przydzielanych na stercie).

Informacje z komentarza w Include / object.h .

Gdy próbujesz ustawić nową wartość some_obj.__class__, object_set_classwywoływana jest funkcja. Jest dziedziczony z PyBaseObject_Type , patrz /* tp_getset */pole. Ta funkcja sprawdza : czy nowy typ może zastąpić stary typ some_obj?

Weź przykład:

class A:
    pass

class B:
    pass

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

Pierwszy przypadek:

a.__class__ = B 

Typem aobiektu jest Atyp sterty, ponieważ jest on przydzielany dynamicznie. Jak również B. W a„s typ jest zmieniane bez problemu.

Drugi przypadek:

o.__class__ = B

Typ ojest typem wbudowanym object( PyBaseObject_Type). To nie jest typ sterty, więc TypeErrorjest podnoszony:

TypeError: __class__ assignment only supported for heap types or ModuleType subclasses.
MiniMax
źródło
4

Możesz zmienić tylko __class__na inny typ, który ma ten sam układ wewnętrzny (C) . Środowisko wykonawcze nawet nie zna tego układu, chyba że sam typ jest dynamicznie przydzielany („typ sterty”), więc jest to warunek konieczny, który wyklucza wbudowane typy jako źródło lub miejsce docelowe. Musisz także mieć ten sam zestaw o __slots__takich samych nazwach.

Davis Herring
źródło