Как работает наследование в метаклассе Python?

Предположим, у меня есть собственный метакласс и связанный с ним класс:

class Meta(type): pass
class A(metaclass=Meta): pass

Насколько я понимаю, в конце оператора class A выполняются следующие шаги:

  1. Звоните Meta('A', (), {}).
  2. Поскольку шаг 1 является встроенным вызовом, это означает, что будет вызван type.__call__(...). Это потому, что type связано с Meta.__class__.
  3. type.__call__(...), в свою очередь, запускает два других метода (__new__ и __init__).
  4. Если Meta определил один или оба этих метода, то внутри type.__call__ эти методы будут вызываться как Meta.__new__(...) и/или Meta.__init__(...).
  5. класс A создан и связан с классом Meta (A.__class__).

Теперь предположим, что у меня есть подкласс A:

class Meta(type): pass
class A(metaclass=Meta): pass
class B(A): pass

Верны ли следующие шаги в конце оператора class B?

  1. Назовите type('B', (), {}) вместо Meta, потому что B.__class__ это type.
  2. Вызовите type.__call__(...), который, в свою очередь, запустит два других метода (__new__ и __init__).
  3. type.__new__(type, 'B', (A,), {}).
  4. type.__init__(cls, 'B', (A,), {}).

Предположим, что приведенные выше шаги верны (в чем я сомневаюсь), не должен ли B.__class__ давать type вместо Meta? Я полагаю, что B создается с метаклассом type по умолчанию. Но распечатка B.__class__ дает Meta вместо type.

print(B.__class__) #<class '__main__.Meta'>

Также, если я вручную создам класс с A в качестве родителя, снова созданный класс будет связан с классом Meta.

C = type.__call__(type, 'C', (A,), {})
print(C.__class__) #<class '__main__.Meta'>

#or

D = type.__new__(type, 'D', (A,), {})
print(D.__class__) #<class '__main__.Meta'>

Мой вопрос заключается в том, как Python создает class B/C и как B/C связан с Meta?


person Henry Tjhia    schedule 02.01.2021    source источник


Ответы (2)


Итак --- несколько сбивающий с толку вопрос, на который можно ответить и несколько упростить, просто запустив несколько примеров в интерактивном режиме.

Но для начала, когда вы заявляете:

type.__call__(...) in turn run two other methods (a __new__ and a __init__).

Это упрощение того, что происходит.

Когда мы создаем новый класс, например, при разрешении оператора класса class A:, type.__call__ вызывается нормально. Но этот вызов ищется в классе самого Meta. То есть метакласс Meta, который по умолчанию равен type.

Потерпите меня: когда мы говорим об обычном классе E без настраиваемого метакласса, и вы создаете экземпляр, выполняя E(), Python ищет метод __call__ в классе, экземпляром которого является E: то есть его метаклассом. Так как это типа, то называется type.__call__. Это type.__call__, который вызывает методы __new__ и __init__, как вы сказали, но не только для метаклассов: он организует эти вызовы в любом экземпляре объекта - точно такой же механизм используется в любом экземпляре объекта в Python: как обычные объекты, так и классы:



In [178]: class MetaMeta(type): 
     ...:     def __call__(metacls, *args, **kw): 
     ...:         print("Now at the meta-meta class") 
     ...:         return super().__call__(*args, **kw) 
     ...:                         

In [179]: class EmptyMeta(type, metaclass=MetaMeta): 
     ...:     def __call__(cls, *args, **kw): 
     ...:         print("At the metaclass __call__") 
     ...:         return super().__call__(*args, **kw) 
     ...:          
     ...:      
     ...:                         

In [180]: class A(metaclass=EmptyMeta): 
     ...:     pass 
     ...:                         
Now at the meta-meta class

In [181]: a = A()                 
At the metaclass __call__

In [182]: class Direct(metaclass=MetaMeta): pass                     

In [183]: Direct()                
Now at the meta-meta class
Out[183]: <__main__.Direct at 0x7fa66bc72c10>


Итак, вкратце: при создании класса A, который является экземпляром Meta, вызывается метод __call__ класса Meta. Это вызовет __init__ и __new__ в метаклассе Meta. Если они не определены, обычный поиск атрибутов вызовет эти методы в суперклассе Meta, который также является типом.

Теперь, переходя к вашему вопросу: когда кто-то наследуется от класса с настраиваемым метаклассом, таким как ваш класс B, Python берет наиболее производный метакласс в своих суперклассах в качестве своего собственного метакласса, а не type. Нет необходимости явно объявлять пользовательский метакласс. На практике это то, что делает метакласс необходимым, а не просто декораторами классов: они влияют только на класс, в котором они объявлены, и не действуют на последующие подклассы.


In [184]: class B(A): pass        
Now at the meta-meta class

In [185]: B()                     
At the metaclass __call__
Out[185]: <__main__.B at 0x7fa6682ab3a0>

In [186]: B.__class__             
Out[186]: __main__.EmptyMeta

Даже при явном вызове type вместо оператора class метакласс производного класса будет метаклассом суперкласса. Обратите внимание, однако, что в этом случае мы жестко кодируем вызов метамета-класса на type.__new__, а пользовательский метакласс метакласса игнорируется:

                               
In [187]: C = type("C", (A, ), {})

In [188]: C()                     
At the metaclass __call__
Out[188]: <__main__.C at 0x7fa653cb0d60>


Если вы хотите программно создать класс, который имеет пользовательский мета-метакласс (упаси бог, он понадобится для чего-либо, кроме целей обучения), в модуле types есть специальный вызов, который делает это:


In [192]: import types            

In [193]: D = types.new_class("D", (A,), {})                         
Now at the meta-meta class

In [194]: D()                     
At the metaclass __call__
Out[194]: <__main__.D at 0x7fa6682959a0>

И в заключение отметим, что если суперклассы класса имеют расходящиеся метаклассы, Python вообще откажется создавать класс. Это несколько распространено в коде реального мира, когда люди пытаются создать абстрактные классы (которые используют пользовательский метакласс) с базовым классом в какой-либо структуре с ORM, которые обычно также имеют пользовательский метакласс:


                                                                                                                                         
In [203]: class Meta1(type): pass 

In [204]: class Meta2(type): pass 

In [205]: class A(metaclass=Meta1): pass                             

In [206]: class B(metaclass=Meta2): pass                             

In [207]: class C(A, B): pass     
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-207-1def53cc27f4> in <module>
----> 1 class C(A, B): pass

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

Это можно исправить, создав производный метакласс, который наследуется от метаклассов в обеих ветвях-предках (для этого требуется, чтобы оба метакласса хорошо себя вели, используя super() вместо вызовов жесткого кодирования type, но это в случае с хорошо поддерживаемыми и популярными фреймворками):


In [208]: class Meta3(Meta1, Meta2): pass                            

In [209]: class C(A, B, metaclass=Meta3): pass                       

In [210]:
person jsbueno    schedule 02.01.2021

Вызовите type('B', (), {}) вместо Meta, потому что B.class является типом.

Как вы заметили позже, это не так.

>>> class Meta(type): pass
...
>>> class A(metaclass=Meta): pass
...
>>> class B(A): pass
...
>>> type(B)
<class '__main__.Meta'>
>>>

Мой вопрос в том, как Python создает класс B/C и как B/C связан с Meta?

Если класс X наследует класс Y, то метакласс X совпадает с метаклассом Y. Подробную информацию можно найти в документации по модели данных.

Из документов:

Соответствующий метакласс для определения класса определяется следующим образом:

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

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

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

Наиболее производный метакласс выбирается из явно указанного метакласса (если есть) и метаклассов (т. е. типа (cls)) всех указанных базовых классов. Наиболее производным метаклассом является тот, который является подтипом всех этих метаклассов-кандидатов. Если ни один из метаклассов-кандидатов не соответствует этому критерию, определение класса завершится ошибкой TypeError.

person Ekrem Dinçel    schedule 02.01.2021