Порядок вызова метакласса python3 - PullRequest
0 голосов
/ 11 ноября 2018

Я в замешательстве, пытаясь понять порядок, в котором metaclass создает экземпляр класса.Согласно этой диаграмме ( источник ), enter image description here

Я ввожу следующие коды , чтобы проверить это.

class Meta(type):
    def __call__(self):
        print("Meta __call__")
        super(Meta, self).__call__()

    def __new__(mcs, name, bases, attrs, **kwargs):
        print("Meta __new__")
        return super().__new__(mcs, name, bases, kwargs)

    def __prepare__(msc, name, **kwargs):
        print("Meta __prepare__")
        return {}

class SubMeta(Meta):
    def __call__(self):
        print("SubMeta __call__!")
        super().__call__()

    def __new__(mcs, name, bases, attrs, **kwargs):
        print("SubMeta __new__")
        return super().__new__(mcs, name, bases, kwargs)

    def __prepare__(msc, name, **kwargs):
        print("SubMeta __prepare__")
        return Meta.__prepare__(name, kwargs)

class B(metaclass = SubMeta):
    pass

b = B()

Однако результат выглядит не так, как следует ниже.

SubMeta __prepare__
Meta __prepare__
SubMeta __new__
Meta __new__
SubMeta __call__!
Meta __call__

Любая помощь будет оценена.

Ответы [ 2 ]

0 голосов
/ 12 ноября 2018

Несмотря на длинный ответ @ torek, с множеством других подробностей о создании классов, то, что вы собрали в этом вопросе, в основном верно.

Единственное, что не так в вашем коде, которое, вероятно, озадачило вас, - это то, что класс, который вы называете Meta, должен быть сам по себе метаклассом из SubMeta, а не его родителем.

Просто измените Submeta объявление на:

class SubMeta(type, metaclass=Meta):
    ...

(Нет необходимости в том, чтобы он также наследовался от «Meta» - он может быть получен только от type. Иначе стоит подумать о настройке на type.__call__, которая в то же время была бы полезна для создания экземпляров ваших классов (то есть когда вызывается SubMeta.__call__), и сами ваши классы (Meta.__call__ называемые))

Вот еще один, более короткий пример, который я только что набрал в терминале. Извините за несоответствия именования и за то, что они менее полны - но это показывает главное:

class M(type):
    def __call__(mmcls, *args, **kwargs):
        print("M's call", args, kwargs)
        return super().__call__(*args, **kwargs)

class MM(type, metaclass=M):
    def __prepare__(cls, *args, **kw):
        print("MM Prepare")
        return {}
    def __new__(mcls, *args, **kw):
        print("MM __new__")
        return super().__new__(mcls, *args, **kw)

class klass(metaclass=MM):
    pass

После обработки тела klass вывод Python был:

MM Prepare
M's call ('klass', (), {'__module__': '__main__', '__qualname__': 'klass'}) {}
MM __new__

1028 * Более того * Как вы можете видеть из этого, с мета-мета-классом можно настроить порядок вызовов и параметры для метакласса __init__ и __new__, , но есть еще шаги, которые могут не должны быть настроены из чистого Python-кода и потребуют собственных вызовов API (и, возможно, необработанных манипуляций со структурой объектов): Невозможно контролировать вызов __prepare__ Нельзя управлять вызовом __init_subclass__ на созданных классах Можно контролировать, когда дескрипторы __set_name__ называются Последние два элемента имеют место после возврата мета-мета __call__ и перед возобновлением потока в модуль, где находится модуль класса.

0 голосов
/ 11 ноября 2018

Хитрость, выявленная

Обновление 2: Исходя из поведения, тот факт, что M0.__call__ вызывается ниже , должен быть побочным эффектом этой строки в builtin__build_class в исходник CPython (Python/bltinmodule.c).

Чтобы определить класс, имеющий метакласс, мы называем метаклассы __prepare__, __new__ и __init__ как обычно. Это создает класс - в примере ниже, Meta - который можно вызывать, но его внутренний слот PyFunction_GET_CODE указывает не на его собственный __call__, а скорее на его метакласс __call__. Следовательно, если мы вызываем Meta() (объект метакласса), мы вызываем M0.__call__:

print("call Meta")
print("Meta returns:", Meta('name', (), {}))
print("finished calling Meta")

производит:

call Meta
M0 __call__: mmcls=<class '__main__.Meta'>, args=('name', (), {}), kwargs={}
Meta __new__: mcs=<class '__main__.Meta'>, name='name', bases=(), attrs={}, kwargs={}
Meta __init__: mcs=<class '__main__.name'>, name='name', bases=(), attrs={}, kwargs={}
Meta returns: <class '__main__.name'>
finished calling Meta

Другими словами, мы видим, что Meta действует как type, но это (довольно волшебно и не очень хорошо задокументировано) вызывает M0.__call__. Это, без сомнения, связано с поиском __call__ в типе класса, а не в экземпляре класса (и действительно, нет экземпляра, кроме того, который мы создаем). На самом деле это общий случай: он выпадает из факта, что мы вызываем __call__ для типа из Meta, а тип Meta равен M0:

print("type(Meta) =", type(Meta))

печать:

type(Meta) = <class '__main__.M0'>

, который объясняет, откуда это происходит. (Я все еще думаю, что это должно быть подчеркнуто в документации, которая также должна описывать ограничения на типизацию метаклассов - они применяются в _calculate_winner в Lib/types.py и, как код C, в _PyType_CalculateMetaclass в объектах / typeobject.c .)

Обновлен оригинальный ответ

Я не знаю, откуда взялась ваша диаграмма, но это неверно. ОБНОВЛЕНИЕ: На самом деле вы можете иметь метакласс для своего метакласса; смотрите ответ jsbueno , и я обновил пример ниже. Новые предложения / текст выделены жирным шрифтом , , за исключением заключительного раздела, описывающего мое недоумение по поводу очевидного отсутствия документации.

В вашем существующем коде метакласса есть хотя бы одна ошибка. Наиболее важно, что __prepare__ должен быть методом класса. См. Также Использование метода __call__ метакласса вместо __new __? и PEP 3115 . И, чтобы использовать мета-мета-класс, ваш метакласс должен иметь собственный метакласс, не базовый класс.

Ответ Криса содержит правильные определения. Но есть некоторые неудачные асимметрии между аргументами метода метакласса и аргументами метода класса, которые я проиллюстрирую ниже.

Еще одна вещь, которая может помочь: обратите внимание, что метод метакласса __prepare__ вызывается перед созданием любых экземпляров класса B: , он вызывается при определении самой class B. Чтобы показать это, вот исправленный метакласс-класс. Я также добавил еще несколько иллюстраторов. Я также добавил мета-метакласс, основываясь на ответе jsbueno. Я не могу найти официальную документацию Python по этому вопросу, но я обновил вывод ниже.

class M0(type):
    def __call__(mmcls, *args, **kwargs):
        print("M0 __call__: mmcls={!r}, "
              "args={!r}, kwargs={!r}".format(mmcls, args, kwargs))
        return super().__call__(*args, **kwargs)

class Meta(type, metaclass=M0):
    def __call__(cls, *args, **kwargs):
        print("Meta __call__: cls={!r}, "
              "args={!r}, kwargs={!r}".format(cls, args, kwargs))
        return super().__call__(*args, **kwargs)

    def __new__(mcs, name, bases, attrs, **kwargs):
        print("Meta __new__: mcs={!r}, name={!r}, bases={!r}, "
              "attrs={!r}, kwargs={!r}".format(mcs, name, bases, attrs, kwargs))
        return super().__new__(mcs, name, bases, attrs)

    def __init__(mcs, name, bases, attrs, **kwargs):
        print("Meta __init__: mcs={!r}, name={!r}, bases={!r}, "
              "attrs={!r}, kwargs={!r}".format(mcs, name, bases, attrs, kwargs))
        super().__init__(name, bases, attrs, **kwargs)

    @classmethod
    def __prepare__(cls, name, bases, **kwargs):
        print("Meta __prepare__: name={!r}, "
              "bases={!r}, kwargs={!r}".format(name, bases, kwargs))
        return {}

print("about to create class A")
class A(metaclass=Meta): pass
print("finished creating class A")

print("about to create class B")

class B(A, metaclass=Meta, foo=3):
    @staticmethod
    def __new__(cls, *args, **kwargs):
        print("B __new__: cls={!r}, "
              "args={!r}, kwargs={!r}".format(cls, args, kwargs))
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print("B __init__: args={!r}, kwargs={!r}, ".format(args, kwargs))

print("finished creating class B")

print("about to create instance b = B()")
b = B('hello', bar=7)
print("finished creating instance b")

Теперь давайте посмотрим, что произойдет, когда я запущу это, и разберем каждый фрагмент:

$ python3.6 meta.py
about to create class A
Meta __prepare__: name='A', bases=(), kwargs={}
M0 __call__: mmcls=<class '__main__.Meta'>, args=('A', (), {'__module__': '__main__', '__qualname__': 'A'}), kwargs={}
Meta __new__: mcs=<class '__main__.Meta'>, name='A', bases=(), attrs={'__module__': '__main__', '__qualname__': 'A'}, kwargs={}
Meta __init__: mcs=<class '__main__.A'>, name='A', bases=(), attrs={'__module__': '__main__', '__qualname__': 'A'}, kwargs={}
finished creating class A

Чтобы создать сам класс A, Python сначала вызывает метакласс __prepare__, передавая ему имя класса (A), список базовых классов (пустой кортеж - он называется списком, но на самом деле кортеж), и любые ключевые аргументы (нет). Как отмечает PEP 3115, метакласс должен возвращать словарь или dict -подобный объект; это просто возвращение пустого словаря, так что нам здесь хорошо.

(я здесь не печатаю cls, но если вы это сделаете, вы увидите, что это просто <class '__main__.Meta'>.)

Затем, получив словарь от __prepare__, Python сначала вызывает метамета __call__, то есть M0.__call__, передавая весь набор аргументов как кортеж args. Затем он заполняет словарь с поддержкой __prepare__ всеми атрибутами для класса, передавая его как attrs метаклассу __new__ и __init__. Если вы напечатаете id словаря, возвращенного из __prepare__ и переданного в __new__ и __init__, вы увидите, что все они совпадают.

* +1121 * Si,У класса A нет методов или элементов данных, здесь мы видим только магические атрибуты __module__ и __qualname__. Мы также не видим ключевых аргументов, поэтому теперь давайте перейдем к созданию класса B:
about to create class B
Meta __prepare__: name='B', bases=(<class '__main__.A'>,), kwargs={'foo': 3}
M0 __call__: mmcls=<class '__main__.Meta'>, args=('B', (<class '__main__.A'>,), {'__module__': '__main__', '__qualname__': 'B', '__new__': <staticmethod object at 0x800ad0a58>, '__init__': <function B.__init__ at 0x800ad2840>, '__classcell__': <cell at 0x800a749d8: empty>}), kwargs={'foo': 3}
Meta __new__: mcs=<class '__main__.Meta'>, name='B', bases=(<class '__main__.A'>,), attrs={'__module__': '__main__', '__qualname__': 'B', '__new__': <staticmethod object at 0x800ad0940>, '__init__': <function B.__init__ at 0x800ad27b8>, '__classcell__': <cell at 0x800a745b8: empty>}, kwargs={'foo': 3}
Meta __init__: mcs=<class '__main__.B'>, name='B', bases=(<class '__main__.A'>,), attrs={'__module__': '__main__', '__qualname__': 'B', '__new__': <staticmethod object at 0x800ad0940>, '__init__': <function B.__init__ at 0x800ad27b8>, '__classcell__': <cell at 0x800a745b8: Meta object at 0x802047018>}, kwargs={'foo': 3}
finished creating class B

Этот довольно интересный. Теперь у нас есть один базовый класс, а именно __main__.A. Класс B также определяет несколько методов (__new__ и __init__), и мы видим их в словарях attrs, передаваемых метаклассам __new__ и __init__ (которые, помните, являются только что заполненными в настоящее время) словарь, возвращаемый метаклассом __prepare__). Как и прежде, передача происходит через метамета-класс M0.__call__. Мы также видим один ключевой аргумент повсюду, {'foo': 3}. В словаре атрибутов мы также можем наблюдать волшебную запись __classcell__: см. Предоставьте пример __classcell__ для метакласса Python 3.6 для краткого описания того, о чем идет речь, но, скорее всего, супер -короткий, это для того, чтобы заставить super() работать.

Аргумент ключевого слова передается всем трем методам метакласса, а также мета-мета-классу. (Я не совсем уверен, почему. Обратите внимание, что изменение словаря в любом методе metaclass никак не влияет на него, так как это копия каждого аргумента исходного ключевого слова каждый раз. Однако мы может изменить его в мета-мета-классе: добавьте kwargs.pop('foo', None) к M0.__call__, чтобы наблюдать это. )

Теперь, когда у нас есть классы A и B, мы можем приступить к процессу создания фактического экземпляра класса B. Теперь мы видим вызванный метакласс __call__ (не мета-мета-класс):

about to create instance b = B()
Meta __call__: cls=<class '__main__.B'>, args=('hello',), kwargs={'bar': 7}

Можно изменить переданный args или kwargs, но я этого не делаю; приведенный выше пример кода вызывает type.__call__(cls, *args, **kwargs) (через магию super().__call__). Это, в свою очередь, вызывает B.__new__ и B.__init__:

B __new__: cls=<class '__main__.B'>, args=('hello',), kwargs={'bar': 7}
B __init__: args=('hello',), kwargs={'bar': 7}, 
finished creating instance b

, который завершает реализацию нового экземпляра класса B, который мы затем связываем с именем b.

Обратите внимание, что B.__new__ говорит:

return super().__new__(cls)

поэтому мы вызываем object.__new__ для создания экземпляра - это более или менее требование для всех версий Python; вы можете «обмануть» только когда вернете одноэлементный экземпляр (в идеале тот, который не подлежит изменению). type.__call__ вызывает B.__init__ для этого объекта, передавая аргументы и ключевые слова-аргументы, которые мы передали. Если мы заменим Meta s __call__ на:

    def __call__(cls, *args, **kwargs):
        print("Meta __call__: cls={!r}, "
              "args={!r}, kwargs={!r}".format(cls, args, kwargs))
        return object.__new__(cls)

мы увидим, что B.__new__ и B.__init__ никогда не называются:

about to create instance b = B()
Meta __call__: cls=<class '__main__.B'>, args=('hello',), kwargs={'bar': 7}
finished creating instance b

По сути, это создаст бесполезный / неинициализированный экземпляр b. Поэтому очень важно, чтобы метод метакласса __call__ вызывал базовый класс __init__, обычно вызывая type.__call__ через super().__call__. Если базовый класс имеет __new__, метакласс должен сначала вызвать его, опять же, обычно вызывая type.__call__.

Примечание: что документация говорит

Цитировать раздел 3.3.3.6:

Как только пространство имен класса заполняется путем выполнения тела класса, объект класса создается путем вызова metaclass(name, bases, namespace, **kwds) (дополнительные ключевые слова, переданные здесь, такие же, как переданные в __prepare__).

Это объясняет вызов Meta.__call__ при создании b как экземпляра класса B, но не факт, что Python сначала вызывает M0.__call__ перед вызовом Meta.__new__ и Meta.__init__ при создании классов A и B сами.

В следующем абзаце упоминается запись __classcell__; затем описывается использование хуков __set_name__ и __init_subclass__. Ничто здесь не говорит нам, как или почему Python вызывает M0.__call__ на данный момент.

Ранее в разделах с 3.3.3.3 по 3.3.3.5 в документации описывался процесс определения метакласса, подготовки пространства имен класса и выполнения тела класса. Именно здесь должно быть описано действие мета-метакласса , но это не так.

Несколько дополнительных разделов описывают несколько дополнительных ограничений.Одним из важных является 3.3.10, в котором говорится о том, как специальные методы находят через тип объекта, минуя как обычный поиск атрибутов-членов, так и даже (иногда) метакласс getattribute, говоря:

Обход *Таким образом, механизм 1242 * обеспечивает значительные возможности для оптимизации скорости в интерпретаторе за счет некоторой гибкости в обработке специальных методов (специальный метод должен быть должен быть установлен на самом объекте класса, чтобыпоследовательно вызывается интерпретатором).

Обновление 2: В этом-то и заключается секрет уловки: специальный метод __call__ находится через тип типа.Если у метакласса есть метакласс, мета-метакласс предоставляет слот __call__;в противном случае тип метакласса - type, поэтому слот __call__ - type.__call__.

...