Чому я не можу змінити атрибут __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 має коментар у Object / 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.

Наскільки нам відомо, весь код, що перебуває за викладеним нижче твердженням "якщо", буде правильно обробляти класи, які не є HEAPTYPE, і перевірка HEAPTYPE потрібна лише для захисту тієї підмножини класів, які не є HEAPTYPE, для яких інтерпретатор припустив, що всі екземпляри справді незмінні.

Пояснення:

CPython зберігає об'єкти двома способами:

Об'єкти - це структури, виділені на купі. Для використання об’єктів застосовуються спеціальні правила, щоб забезпечити їх належне збирання сміття. Об'єкти ніколи не виділяються статично або на стеку; доступ до них повинен здійснюватися лише за допомогою спеціальних макросів і функцій. (Типи об'єктів є винятками з першого правила; стандартні типи представлені статично ініціалізованими об'єктами типу, хоча робота над об'єднанням типу / класу для Python 2.2 дозволила також мати об’єкти типу виділеного купи).

Інформація з коментаря у Включити / 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.

4

Ви можете змінити лише __class__інший тип, який має той же внутрішній (C) макет . Виконання навіть не знає цього макета, якщо сам тип динамічно не розподіляється ("тип купи"), тому це необхідна умова, яка виключає вбудовані типи як джерело чи призначення. Ви також повинні мати той самий набір __slots__з тими ж іменами.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.