Эксперименты над Питоном
Решил несколько систематизировать знания о классах-метаклассах в Питоне, а то совсем мозги сворачиваются.
Сначала немного теории. В Питоне всё является объектом. Класс — это тоже объект, класс объекта «класс» называется метакласс. То есть создание класса в Питоне точно такой же процесс, как и создание инстанса класса, т.к. класс по сути является инстансом метакласса.
Как происходит создание обычного инстанса:
- У класса вызывается специальный метод
__new__(cls)
с объектом-классом в качестве первого и единственного параметра. Этот метод должен вернуть инстанс класса.Причём не обязательно нашего класса, может и другого, но это более сложный и редкий случай.Таким образом__new__(cls)
— это истинный конструктор инстанса и не существует способа получить инстанс класса в обход__new__()
. - На этом свежем инстансе вызывается специальный метод
__init__(inst)
, который ничего не должен возвращать, а только инициализирует свежесозданный инстанс.Строго говоря он и является конструктором класса, который как правило и надо определять, но есть ситуации, когда инстанс класса определяется в обход__init__()
, например если__new__()
вернул инстанс какого-то другого класса, а не нашего родного, но это опять же граничный случай, я его здесь не рассматриваю.
Теперь меняем в последних двух абзацах слова «класс» и «инстанс» на «метакласс» и «класс» соответственно и получаем то, как рождаются классы в Питоне.
Но это ещё не всё! Есть такой замечательный специальный метод __call__(self, *args, **kwds)
. Если он определён для объекта, то при попытке вызвать объект вызывается именно он, то есть:
obj = SomeClass()
obj("Hello!")
На самом деле неявно вызывает метод инстанса obj.__call__("Hello!")
Добавляем ещё один питонофакт: оператор создания класса в Питоне по сути является оператором вызова функции. То есть когда мы делаем:
obj = SomeClass()
Мы на самом деле вызываем метод SomeClass.__call__()
, который вызывает SomeClass.__new__(SomeClass)
, а потом SomeClass.__init__(inst)
, где inst — результат выполнения SomeClass.__new__()
. Это означает, что:
- если мы похерим
SomeClass.__call__(self)
, то у нас ничего больше работать не будет, инстанс класса мы создать не сможем, - в качестве «класса» мы можем использовать не только собственно класс, но и вообще любую функцию, лишь бы она возвращала инстанс какого угодно класса.
То есть мы можем вообще переопределить всю логику объектной модели Питона, и при определённой сноровке нам за это ничего не будет.
Следующий вопрос, который возникает, это откуда берётся SomeClass.__call__()
? Ведь это метод инстанса, то есть если определить его вот так:
class SomeClass(object):
def __call__(self):
то мы сможем перехватить вызов инстанса этого класса — obj()
— но не самого SomeClass()
. То есть чтобы перехватить вызов класса как функции, надо объявить метод __call__()
в классе класса... В метаклассе.
Дальше привожу пример тестовой программки, которую я написал как раз для выяснения (и прояснения) этой всей крышесносящей байды:
#!/usr/bin/python
class MetaClass(type):
def __new__(cls, name, bases, dicts):
print "meta __new__"
return super(MetaClass, cls).__new__(cls, name, bases, dicts)
def __init__(cls, name, bases, dicts):
print "meta __init__ called"
super(MetaClass, cls).__init__(name, bases, dicts)
def __call__(self):
print "meta __call__ called"
return super(MetaClass, self).__call__()
class TestClass(object):
__metaclass__ = MetaClass
def __new__(cls):
print "__new__ called"
return super(TestClass, cls).__new__(cls)
def __init__(self):
print "__init__ called"
super(TestClass, self).__init__()
def __call__(self):
print "__call__ called"
print "Program start..."
print "Instantiating TestClass..."
tclass = TestClass()
print "TestClass instantiated."
print tclass
print "Finishing program..."
На выходе она выдаёт следующее:
meta __new__ called
meta __init__ called
Program start...
Instantiating TestClass...
meta __call__ called
__new__ called
__init__ called
TestClass instantiated.
<__main__.TestClass object at 0xb7cd7fcc>
Finishing program...
При этом если заблокировать вызов super(MetaClass, self).__call__()
в методе MetaClass.__call__()
, то получим следующую картину:
meta __new__ called
meta __init__ called
Program start...
Instantiating TestClass...
meta __call__ called
TestClass instantiated.
None
Finishing program...
То есть инстанс класса вообще не создан, но ошибки не было, т.к. метод __call__()
класса нашего класса вернул None
, что и было принято как корректный инстанс нашего класса.
Отсюда следуют выводы:
- Классы в Питоне создаются практически динамически как инстансы метаклассов ещё до запуска основной программы.
- Создание классов как инстансов метаклассов происходит точно так же, как создание инстансов на основе классов.
- Руководит созданием инстансов/классов метод
__call__()
класса/метакласса на основе которого создаётся данный инстанс/класс, так что переопределив его мы можем полностью изменить логику создания инстансов/классов как нам заблагорассудиться (хотя делать это строго не рекомендуется). - Однако сделать то же самое с метаклассами уже не получится: метаметаклассов уже нет.
Что вполне логично, а то можно совсем с ума сойти. - Сразу после описания класса в структуре
class ClassName(bases): ...
вызываетсяcls = MetaClass.__new__(metacls, ClassName, bases, dict)
, а потомMetaClass.__init__(cls, ClassName, bases, dict)
, и возвращает cls, который есть класс, который по сути инстанс класса MetaClass. Потом при создании инстанса класса ClassName вызываетсяMetaClass.__call__()
, который вызваетinst = ClassName.__new__(cls)
иClassName.__init__(inst, ...)
. Или короче: MetaClass.__new__(cls)
иMetaClass.__init__()
вызываются сразу после описания класса,MetaClass.__call__()
вызывается при создании инстанса класса.- Метод
__new__
является статическим методом, а не методом класса, поэтому ему всегда надо явно передавать класс создаваемого инстанса в качестве первого параметра. - Самым корневым классом для классов является класс
object
, самым корневым метаклассом для метаклассов являетсяtype
, классobject
является по сути инстансом метаклассаtype
. - В качестве метакласса может быть использована любая функция (или вообще любая вызываемая структура), которая на входе принимает (classname, bases, dict), а на выходе отдаёт класс.
- Функция — это тоже объект, у которого определён метод
__call__()
, поэтому её и можно вызывать.Впрочем, метод__call__()
это тоже функция... У попа была собака...
Ссылки по теме: