Нет - атрибут __class__
является фундаментальной информацией о макете всех объектов Python, которые «видны» на самом уровне C API.И это то, что проверяется вызовом type
.
. Это означает, что каждый объект Python имеет слот в своей компоновке в памяти с пространством для одного указателя на объект Python, который является объектом этого объекта.учебный класс.
Даже если вы используете ctypes или другие средства, чтобы переопределить защиту для этого слота и изменить ее из кода Python (поскольку изменение obj.__class__
с помощью =
защищено на уровне C), его изменение эффективно изменяет объектtype: значение в слоте __class__
является классом объекта, и метод test
будет выбран из класса там (Bar) в вашем примере.
Однако здесь есть дополнительная информация: inвся документация type(obj)
рассматривается как эквивалентная obj.__class__
- однако, если класс объектов определяет дескриптор с именем __class__
, он используется, когда используется форма obj.__class__
.Однако type(obj)
будет непосредственно проверять слот __class__
экземпляра и возвращать истинный класс.
Таким образом, это может «лгать» коду, используя obj.__class__
, но не type(obj)
:
class Bar:
def test(self):
return 2
class Foo:
def test(self):
return 1
@property
def __class__(self):
return Bar
Свойство в метаклассе
Попытка возиться с созданием дескриптора __class__
в самом метаклассе Foo
будет грязной - и type(Foo())
, и repr(Foo())
сообщат instance of Bar
, но "реальным" классом объекта будет Foo.В некотором смысле, да, это делает type(Foo())
ложью, но не так, как вы думали, - type (Foo ()) выведет repr из Bar()
, но это - Foo
, что испортилосьвверх, из-за деталей реализации внутри type.__call__
:
In [73]: class M(type):
...: @property
...: def __class__(cls):
...: return Bar
...:
In [74]: class Foo(metaclass=M):
...: def test(self):
...: return 1
...:
In [75]: type(Foo())
Out[75]: <__main__.Bar at 0x55665b000578>
In [76]: type(Foo()) is Bar
Out[76]: False
In [77]: type(Foo()) is Foo
Out[77]: True
In [78]: Foo
Out[78]: <__main__.Bar at 0x55665b000578>
In [79]: Foo().test()
Out[79]: 1
In [80]: Bar().test()
Out[80]: 2
In [81]: type(Foo())().test()
Out[81]: 1
Изменение type
само по себе
Поскольку никто не "импортирует" type
из любого места, а просто использует встроенныйсам по себе, можно установить встроенную функцию вызова type
для сообщения о ложном классе - и он будет работать для всего кода Python в одном процессе, полагаясь на вызов type
:
original_type = __builtins__["type"] if isinstance("__builtins__", dict) else __builtins__.type
def type(obj_or_name, bases=None, attrs=None, **kwargs):
if bases is not None:
return original_type(obj_or_name, bases, attrs, **kwargs)
if hasattr(obj_or_name, "__fakeclass__"):
return getattr(obj_or_name, "__fakeclass__")
return original_type(obj_or_name)
if isinstance(__builtins__, dict):
__builtins__["type"] = type
else:
__builtins__.type = type
del type
Здесь есть одна хитрость, которую я не нашел в документации: при работе с __builtins__
в программе он работает как словарь.Однако в интерактивной среде, такой как Python Repl или Ipython, это модуль - для извлечения исходного type
и записи измененной версии в __builtins__
необходимо учитывать это - приведенный выше код работает в обоих направлениях.
И тестирование этого (я импортировал приведенный выше фрагмент из файла .py на диск):
>>> class Bar:
... def test(self):
... return 2
...
>>> class Foo:
... def test(self):
... return 1
... __fakeclass__ = Bar
...
>>> type(Foo())
<class '__main__.Bar'>
>>>
>>> Foo().__class__
<class '__main__.Foo'>
>>> Foo().test()
1
Хотя это работает в демонстрационных целях, замена встроенного типа вызывала «диссонанс»это оказалось фатальным в более сложной среде, такой как IPython: Ipython аварийно завершит работу и немедленно прекратит работу при запуске приведенного выше фрагмента.