Почему я не могу изменить атрибут __class__ экземпляра объекта?

10
class A(object):
    pass

class B(A):
    pass

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

Хотя я могу изменить a.__class__, я не могу сделать то же самое с o.__class__(это выдает TypeErrorошибку). Почему?

Например:

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)

Я знаю, что, как правило, это не очень хорошая идея, так как это может привести к очень странному поведению, если оно обрабатывается неправильно. Это просто ради любопытства.

Риккардо Букко
источник
3
Встроенные типы жертвуют динамикой пользовательского типа по соображениям эффективности. Обратите внимание, что другая дополнительная оптимизация - это слоты, которые также предотвратят это.
juanpa.arrivillaga

Ответы:

6

CPython имеет комментарий в Objects / typeobject.c по этой теме:

В версиях CPython до 3.5 код compatible_for_assignmentне был настроен для правильной проверки совместимости макета памяти, слотов и т. Д. Для классов, отличных от HEAPTYPE, поэтому мы просто запретили __class__присваивание в любом случае, если это не HEAPTYPE -> HEAPTYPE.

В течение цикла разработки 3.5 мы исправили код, compatible_for_assignmentчтобы правильно проверить совместимость между произвольными типами, и начали разрешать __class__присваивание во всех случаях, когда старый и новый типы действительно имели совместимые слоты и расположение памяти (независимо от того, были ли они реализованы как HEAPTYPE). или нет).

Однако непосредственно перед выпуском 3.5 мы обнаружили, что это приводит к проблемам с неизменяемыми типами, такими как int, где интерпретатор предполагает, что они неизменны, и интернирует некоторые значения. Раньше это не было проблемой, потому что они действительно были неизменяемыми - в частности, все типы, в которых интерпретатор применял этот трюк интернирования, также были статически распределены, поэтому старые правила HEAPTYPE «случайно» мешали им разрешить __class__присваивание. Но с изменениями в __class__назначении мы начали разрешать такой код

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

(см. https://bugs.python.org/issue24912 ).

Теоретически правильным решением было бы определить, какие классы полагаются на этот инвариант, и каким-то образом запретить __class__присваивание только для них, возможно, с помощью некоторого механизма, такого как новый флаг Py_TPFLAGS_IMMUTABLE (подход «черного списка»). Но на практике, поскольку эта проблема не была замечена в конце цикла 3.5 RC, мы используем консервативный подход и восстанавливаем ту же самую проверку HEAPTYPE-> HEAPTYPE, которую мы использовали, плюс «белый список». На данный момент белый список состоит только из подтипов ModuleType, поскольку именно эти случаи мотивировали исправление в первую очередь - см. Https://bugs.python.org/issue22986 - и поскольку объекты модуля являются изменяемыми, мы можем быть уверены, что что они точно не интернированы. Так что теперь мы разрешаем HEAPTYPE-> HEAPTYPE или Подтип ModuleType -> Подтип ModuleType.

Насколько нам известно, весь код, за исключением следующего оператора if, будет правильно обрабатывать классы не-HEAPTYPE, а проверка HEAPTYPE необходима только для защиты того подмножества классов не-HEAPTYPE, для которых интерпретатор запекся в предположении, что все случаи действительно неизменны.

Объяснение:

CPython хранит объекты двумя способами:

Объекты - это структуры, расположенные в куче. Особые правила применяются к использованию объектов, чтобы обеспечить их надлежащий сборщик мусора. Объекты никогда не размещаются статически или в стеке; доступ к ним должен осуществляться только через специальные макросы и функции. (Объекты типов являются исключениями из первого правила; стандартные типы представлены статически инициализированными объектами типов, хотя работа по унификации типов / классов для Python 2.2 позволила также иметь объекты типов, выделенные в куче).

Информация из комментария в Include / object.h .

Когда вы пытаетесь установить новое значение some_obj.__class__, object_set_classвызывается функция. Он унаследован от PyBaseObject_Type , см. /* tp_getset */Поле. Эта функция проверяет : может ли новый тип заменить старый тип в some_obj?

Возьмите свой пример:

class A:
    pass

class B:
    pass

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

Первый случай:

a.__class__ = B 

Тип aобъекта - Aтип кучи, поскольку он выделяется динамически. Так же как и B. В aтипе «S меняется без проблем.

Второй случай:

o.__class__ = B

Тип oявляется встроенным типом object( PyBaseObject_Type). Это не тип кучи, поэтому TypeErrorподнимается:

TypeError: __class__ assignment only supported for heap types or ModuleType subclasses.
MiniMax
источник
4

Вы можете перейти только __class__к другому типу, который имеет такую ​​же внутреннюю (C) компоновку . Среда выполнения даже не знает этот макет, если сам тип не выделен динамически («тип кучи»), поэтому это необходимое условие, которое исключает встроенные типы в качестве источника или назначения. Вы также должны иметь одинаковый набор __slots__с одинаковыми именами.

Дэвис Херринг
источник