Пример:
>>> class Cat:
... def meow(self):
... print('meow')
>>> class Dog:
... def bark(self):
... print('woof!')
>>> c = Cat()
>>> c.__class__ = Dog # превращаем котика в собачку!
>>> isinstance(c, Dog)
True
>>> c.bark()
woof!
Но как?
Обратимся к исходникам в
typeobject.c:
static PyGetSetDef object_getsets[] = {
{"__class__", object_get_class, object_set_class,
PyDoc_STR("the object's class")},
{0}
};
static PyObject *
object_get_class(PyObject *self, void *closure)
{
return Py_NewRef(Py_TYPE(self));
}
static int
object_set_class(PyObject *self, PyObject *value, void *closure)
{
// ...
PyTypeObject *newto = (PyTypeObject *)value;
#ifdef Py_GIL_DISABLED
PyInterpreterState *interp = _PyInterpreterState_GET();
_PyEval_StopTheWorld(interp);
#endif
PyTypeObject *oldto = Py_TYPE(self);
// Calls:
// ob->ob_type = newto;
int res = object_set_class_world_stopped(self, newto);
#ifdef Py_GIL_DISABLED
_PyEval_StartTheWorld(interp);
#endif
}
Тут можно увидеть сразу несколько важных моментов:
1. Данное действие совсем-совсем не thread-safe, чтобы его совершить приходится останавливать все остальные треды в режиме noGIL
2. У каждого объекта в Python есть поле
ob_type, где хранится его тип. А значит, тип можно менятьВажно, оба класса, должны быть совместимы, иначе будут ошибки:
- Оба класса должны быть мутабельными
- Оба класса должны иметь совместимый binary layout (см функцию
compatible_for_assignment в `typeobject.c`)-
__slots__ должны быть одинаковымиНо зачем?
Данный хак используется в большом количестве мест в исходниках.
- Например, такое есть прямо в доках
Mock: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.__class__
>>> mock = Mock()
>>> mock.__class__ = dict
>>> isinstance(mock, dict)
True
И его можно менять:
@property
def __class__(self):
if self._spec_class is None:
return type(self)
return self._spec_class
Внутри есть
__setattr__, который вместо .__class__ = X будет менять ._spec_class = X. А свойство будет отражать изменение.-
LazyLoader в importlib делает такое: https://github.com/python/cpython/blob/46f5cbca4c37c57f718d3de0d7f7ddfc44298535/Lib/importlib/util.py#L273
def exec_module(self, module):
"""Make the module load lazily."""
# Threading is only needed for lazy loading, and importlib.util can
# be pulled in at interpreter startup, so defer until needed.
import threading
module.__spec__.loader = self.loader
module.__loader__ = self.loader
loader_state = {}
loader_state['__dict__'] = module.__dict__.copy()
loader_state['__class__'] = module.__class__
loader_state['lock'] = threading.RLock()
loader_state['is_loading'] = False
module.__spec__.loader_state = loader_state
module.__class__ = _LazyModule # <---
- В
annotationlib / typing такое используется, что превращать строковое представление аннотаций в ForwardRef: https://github.com/python/cpython/blob/46f5cbca4c37c57f718d3de0d7f7ddfc44298535/Lib/annotationlib.py#L580-L581
class _Stringifier:
# Must match the slots on ForwardRef, so we can turn an instance of one into an
# instance of the other in place.
__slots__ = _SLOTS
# ...
for obj in globals.stringifiers:
assert isinstance(obj, _Stringifier)
obj.__class__ = ForwardRef
- В
threading используется, чтоб _DummyThread мог притворяться MainThread: https://github.com/python/cpython/blob/46f5cbca4c37c57f718d3de0d7f7ddfc44298535/Lib/threading.py#L1419
def _after_fork(self, new_ident=None):
if new_ident is not None:
self.__class__ = _MainThread
self._name = 'MainThread'
self._daemonic = False
Thread._after_fork(self, new_ident=new_ident)