Почему аргумент ключевого слова метакласса определения класса принимает вызываемый объект?

Фон

документация Python 3 четко описывает, как определяется метакласс класса:

  • если не заданы ни базы, ни явный метакласс, то используется type()
  • если задан явный метакласс и он не является экземпляром type(), то он используется непосредственно как метакласс
  • если экземпляр type() задан как явный метакласс или определены базы, то используется наиболее производный метакласс

Следовательно, по второму правилу можно указать метакласс с помощью callable. Например.,

class MyMetaclass(type):
    pass

def metaclass_callable(name, bases, namespace):
    print("Called with", name)
    return MyMetaclass(name, bases, namespace)

class MyClass(metaclass=metaclass_callable):
    pass

class MyDerived(MyClass):
    pass

print(type(MyClass), type(MyDerived))

Вопрос 1

Является ли метакласс MyClass: metaclass_callable или MyMetaclass? Второе правило в документации гласит, что предоставленный вызываемый объект «используется непосредственно как метакласс». Однако кажется более разумным сказать, что метакласс MyMetaclass, поскольку

  • MyClass и MyDerived имеют тип MyMetaclass,
  • metaclass_callable вызывается один раз, а затем оказывается невосстановимым,
  • производные классы никак не используют (насколько я могу судить) metaclass_callable (они используют MyMetaclass).

вопрос 2

Есть ли что-то, что вы можете сделать с вызываемым объектом, чего нельзя сделать с экземпляром type? Какова цель принятия произвольного вызываемого объекта?


person Neil G    schedule 13.10.2016    source источник


Ответы (3)


Что касается вашего первого вопроса, метакласс должен быть MyMetaclass (это так):

In [7]: print(type(MyClass), type(MyDerived))
<class '__main__.MyMetaclass'> <class '__main__.MyMetaclass'>

Причина в том, что если метакласс не является экземпляром типа, python вызывает метакласс, передавая ему эти аргументы name, bases, ns, **kwds (см. new_class), и поскольку вы возвращаете свой реальный метакласс в этой функции, он получает правильный тип для метакласса.

И по второму вопросу:

Какова цель принятия произвольного вызываемого объекта?

Нет особой цели, на самом деле это природа метаклассов, потому что создание экземпляра из класса всегда вызывает метакласс, вызывая его метод __call__:

Metaclass.__call__()

Это означает, что вы можете передать любой вызываемый объект в качестве своего метакласса. Так, например, если вы протестируете его с помощью вложенной функции, результат будет таким же:

In [21]: def metaclass_callable(name, bases, namespace):
             def inner():
                 return MyMetaclass(name, bases, namespace)
             return inner()
   ....: 

In [22]: class MyClass(metaclass=metaclass_callable):
             pass
   ....: 

In [23]: print(type(MyClass), type(MyDerived))
<class '__main__.MyMetaclass'> <class '__main__.MyMetaclass'>

Для получения дополнительной информации вот как Python создает класс:

Он вызывает функцию new_class, которую он вызывает prepare_class внутри себя, затем, как вы можете видеть, внутри prepare_class python вызывает метод __prepare__ соответствующего метакласса, помимо поиска правильной мета (используя функцию _calculate_meta) и создания соответствующего пространства имен для класса.

Итак, все в одном — это иерархия выполнения методов метакласса:

  1. __prepare__ 1
  2. __call__
  3. __new__
  4. __init__

А вот и исходный код:

# Provide a PEP 3115 compliant mechanism for class creation
def new_class(name, bases=(), kwds=None, exec_body=None):
    """Create a class object dynamically using the appropriate metaclass."""
    meta, ns, kwds = prepare_class(name, bases, kwds)
    if exec_body is not None:
        exec_body(ns)
    return meta(name, bases, ns, **kwds)

def prepare_class(name, bases=(), kwds=None):
    """Call the __prepare__ method of the appropriate metaclass.

    Returns (metaclass, namespace, kwds) as a 3-tuple

    *metaclass* is the appropriate metaclass
    *namespace* is the prepared class namespace
    *kwds* is an updated copy of the passed in kwds argument with any
    'metaclass' entry removed. If no kwds argument is passed in, this will
    be an empty dict.
    """
    if kwds is None:
        kwds = {}
    else:
        kwds = dict(kwds) # Don't alter the provided mapping
    if 'metaclass' in kwds:
        meta = kwds.pop('metaclass')
    else:
        if bases:
            meta = type(bases[0])
        else:
            meta = type
    if isinstance(meta, type):
        # when meta is a type, we first determine the most-derived metaclass
        # instead of invoking the initial candidate directly
        meta = _calculate_meta(meta, bases)
    if hasattr(meta, '__prepare__'):
        ns = meta.__prepare__(name, bases, **kwds)
    else:
        ns = {}
    return meta, ns, kwds


def _calculate_meta(meta, bases):
    """Calculate the most derived metaclass."""
    winner = meta
    for base in bases:
        base_meta = type(base)
        if issubclass(winner, base_meta):
            continue
        if issubclass(base_meta, winner):
            winner = base_meta
            continue
        # else:
        raise TypeError("metaclass conflict: "
                        "the metaclass of a derived class "
                        "must be a (non-strict) subclass "
                        "of the metaclasses of all its bases")
    return winner

1. Обратите внимание, что он неявно вызывается внутри функции new_class и перед возвратом.

person kasravnd    schedule 13.10.2016
comment
Что касается вопроса 2, я не понимаю, как вы отвечаете на мой вопрос: есть ли что-нибудь, что вы можете сделать с вызываемым объектом, чего нельзя сделать с экземпляром type? Какова цель принятия произвольного вызываемого объекта? - person Neil G; 13.10.2016
comment
@NeilG Как я обновил, особой цели нет, это просто природа mtaclasses. - person kasravnd; 13.10.2016
comment
Кажется, это предполагает, что вы можете обойти _calculate_meta, но я не смог этого сделать. - person Neil G; 14.10.2016
comment
@NeilG Да, это для Python 3. Если вы обойдете это, вы не получите правильную мета, как сказано в документе. - person kasravnd; 14.10.2016
comment
Я имею в виду, что я не смог обойти это, указав нетиповой метакласс. (Я все еще вызываю соответствующую ошибку). - person Neil G; 14.10.2016
comment
используя вызываемый объект: class OtherDerived(MyClass, metaclass=other_metaclass_callable): pass - person Neil G; 14.10.2016
comment
@NeilG Это другое дело, я думаю, вы получили metaclass conflict из-за того, что метакласс вашего производного класса не является подклассом метаклассов всех его основ. Как говорит последняя трассировка. - person kasravnd; 14.10.2016
comment
Но это проверяется _calculate_meta, что следует пропустить, если meta не является экземпляром type (обычная функция не является экземпляром type). - person Neil G; 14.10.2016
comment
Я понял. Это потому, что что-то произошло по пути, и Objects/typeobject.c:type_new больше не совпадает с Lib/types.py:new_class. Версия Python условно вызывает _calculate_meta, тогда как версия C вызывает ее безоговорочно. Я считаю реализацию C правильной версией. - person Neil G; 14.10.2016
comment
@NeilG В этом случае он запустит функцию, и, поскольку вы вызываете класс внутри этой функции (с тем же метаклассом, что и один из базовых классов), он вызовет conflict. - person kasravnd; 14.10.2016
comment
@NeilG Я не уверен, запускает ли он здесь код C перед кодом Python. Как я уже сказал, это может быть потому, что после вызова класса внутри функции. - person kasravnd; 14.10.2016
comment
Он вообще не запускает код Python. Он просто вызывает реализацию C, что несовместимо. - person Neil G; 14.10.2016

Ну, type это, конечно, MyMetaClass. metaclass_callable изначально "выбран" в качестве метакласса, так как он был указан в metaclass kwarg и поэтому будет выполнен __call__ (простой вызов функции).

Так получилось, что при вызове будет print, а затем вызовет MyMetaClass.__call__ (который вызывает type.__call__, поскольку __call__ не был переопределен для MyMetaClass). Там выполняется присвоение cls.__class__< /a> до MyMetaClass.

metaclass_callable вызывается один раз, а затем оказывается невосстановимым

Да, он только сначала вызывается, а затем передает управление MyMetaClass. Я не знаю ни одного атрибута класса, который хранит эту информацию.

производные классы никак не используют (насколько я могу судить) metaclass_callable.

Нет, если metaclass не определен явно, наилучшее соответствие для метаклассов из bases (здесь MyClass) будет использоваться (результатом будет MyMetaClass).


Что касается вопроса 2, почти уверен, что все, что вы можете сделать с вызываемым, также возможно, используя экземпляр типа с соответствующим переопределением __call__. Что касается почему, вы можете не захотеть создавать полноценный класс, если вы просто хотите внести небольшие изменения при фактическом создании класса.

person Dimitris Fasarakis Hilliard    schedule 13.10.2016
comment
Из двух вариантов, которые я дал, каков ваш ответ на первый вопрос? Что касается вашего ответа на вопрос 2, можете ли вы привести пример, когда использование вызываемого объекта менее полномасштабно, чем получение от type? - person Neil G; 13.10.2016

Что касается вопроса 1, я думаю, что «метакласс» класса cls следует понимать как type(cls). Такой способ понимания совместим с сообщением об ошибке Python в следующем примере:

>>> class Meta1(type): pass
... 
>>> class Meta2(type): pass
... 
>>> def metafunc(name, bases, methods):
...     if methods.get('version') == 1:
...         return Meta1(name, bases, methods)
...     return Meta2(name, bases, methods)
... 
>>> class C1:
...     __metaclass__ = metafunc
...     version = 1
... 
>>> class C2:
...     __metaclass__ = metafunc
...     version = 2
... 
>>> type(C1)
<class '__main__.Meta1'>
>>> type(C2)
<class '__main__.Meta2'>
>>> class C3(C1,C2): pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Error when calling the metaclass bases
    metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

То есть, согласно сообщению об ошибке, метакласс класса является классом, даже если вызываемый объект, используемый для создания класса, может быть чем угодно.

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

>>> class Mockup(type):
...     def __new__(cls, name, bases, methods):
...         return Meta1(name, bases, methods)
... 
>>> class Foo:
...     __metaclass__ = Mockup
... 
>>> type(Foo)
<class '__main__.Meta1'>
>>> isinstance(Foo, Mockup)
False
>>> Foo.__metaclass__
<class '__main__.Mockup'>

Что касается того, почему Python дает свободу использования любого вызываемого объекта: предыдущий пример показывает, что на самом деле не имеет значения, является ли вызываемый объект типом или нет.

Кстати, вот забавный пример: можно закодировать метаклассы, которые сами по себе имеют метакласс, отличный от type --- назовем его метаметаклассом. Метаметакласс реализует то, что происходит при вызове метакласса. Таким образом можно создать класс с двумя базами, чьи метаклассы не являются подклассами друг друга (сравните с сообщением об ошибке Python в приведенном выше примере!). Действительно, только метакласс результирующего класса является подклассом метакласса баз, и этот метакласс создается на лету:

>>> class MetaMeta(type):
...     def __call__(mcls, name, bases, methods):
...         metabases = set(type(X) for X in bases)
...         metabases.add(mcls)
...         if len(metabases) > 1:
...             mcls = type(''.join([X.__name__ for X in metabases]), tuple(metabases), {})
...         return mcls.__new__(mcls, name, bases, methods)
... 
>>> class Meta1(type):
...     __metaclass__ = MetaMeta
... 
>>> class Meta2(type):
...     __metaclass__ = MetaMeta
... 
>>> class C1:
...     __metaclass__ = Meta1
... 
>>> class C2:
...     __metaclass__ = Meta2
... 
>>> type(C1)
<class '__main__.Meta1'>
>>> type(C2)
<class '__main__.Meta2'>
>>> class C3(C1,C2): pass
... 
>>> type(C3)
<class '__main__.Meta1Meta2'>

Что менее интересно: предыдущий пример не будет работать в Python 3. Если я правильно понимаю, Python 2 создает класс и проверяет, является ли его метакласс подклассом всех его баз, тогда как Python 3 сначала проверяет, существует ли одна база, метакласс которой является суперклассом метаклассов всех остальных баз, и только затем создает новый класс. Это регресс, с моей точки зрения. Но это будет темой нового вопроса, который я собираюсь опубликовать...

Изменить: новый вопрос: здесь

person Simon King    schedule 13.10.2016