Search code examples
pythonpython-3.xinheritancemetaclasspython-3.9

How does inheritance work in Python metaclass?


Suppose, I have a custom metaclass and a class linked to it:

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

From my understanding that at the end of the class A statement, the following steps are executed:

  1. Call Meta('A', (), {}).
  2. Because step 1 is a built-in call, it means that type.__call__(...) will be invoked. This is because type is linked to the Meta.__class__.
  3. type.__call__(...) in turn run two other methods (a __new__ and a __init__).
  4. If Meta defined either or both of these methods, then inside of type.__call__ these methods will be invoked as Meta.__new__(...) and/or Meta.__init__(...).
  5. class A is created and linked to the Meta (A.__class__).

Now, suppose I have a subclass of A:

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

At the end of class B statement, are the following steps correct?

  1. Call type('B', (), {}) instead of Meta, because B.__class__ is type.
  2. Invoke type.__call__(...) which in turn run two other methods (__new__ and __init__).
  3. type.__new__(type, 'B', (A,), {}).
  4. type.__init__(cls, 'B', (A,), {}).

Suppose the above steps is correct (which I doubt), shouldn't B.__class__ give type instead of Meta? My reasoning is that B is created with default type metaclass. But print out B.__class__ gives Meta instead of type.

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

Also if I manually create a class with A as parent, again the created class is linked to the 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'>

My question is how Python create the class B/C and how B/C is linked to the Meta?


Solution

  • So --- a somewhat confusing question that can be answered,and somethat simplified by simply running some examples in the interactive mode.

    But to start, when you state:

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

    It is a simplification of what takes place.

    When we create new class, like in resolving a class statement class A:, type.__call__ is invoked alright. But is this call is searched in the class of Meta itself. That is, the "metaclass" of "Meta" - which by default is type.

    Bear with me: When we are talking about an ordinary class E with no custom metaclass, and you create an instance by doing E() - Python searches for the __call__ method in the class of which E is an instance: that is, its metaclass. As it is type, then type.__call__ is called. It is type.__call__ which calls the __new__ and __init__ methods, as you stated, but not only for metaclasses: it orchestrates these calls in any object instantiation - the exact same mechanism is used in any object instantiation in Python: both ordinary objects and classes:

    
    
    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>
    
    
    

    So, in short: when creating a class A, which is an instance of Meta, the __call__ method of the class of Meta is called. That will call __init__ and __new__ in the metaclass Meta. If those are not defined, ordinary attribute lookup will call these methods in the superclass of Meta, which happens to also be "type".

    Now, moving on on your question: when one inherits from a class with a custom metaclass, like your B class, Python takes the most derived metaclass in its superclasses as its own metaclass, not type. No need to explicitly declare a custom metaclass. That, in practical means, is what makes metaclass needed instead of just Class decorators: these affect only the class where they are declared, and have no effect in further subclasses.

    
    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
    

    Even in an explicit call to type instead of the class statement, the derived class' metaclass will be the metaclass of the superclass. Note, however, that in this case we are hardcoding the call to the "metameta" class to type.__new__ and the "custom metaclass of the metaclass" is ignored:

                                   
    In [187]: C = type("C", (A, ), {})
    
    In [188]: C()                     
    At the metaclass __call__
    Out[188]: <__main__.C at 0x7fa653cb0d60>
    
    
    

    If you want to programmaticaly create a class that has a custom "meta meta class" (God forbid one needing this in anything but learning purposes), there is a special call in the types module that does that:

    
    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>
    

    And to wrap it up, note that if the superclasses of a class have diverging metaclasses, Python will refuse to create a class at all. That is somewhat common in "real world" code, when people try to create Abstract classes (wich usea custom metaclass) with a base class in some framework with an ORM, which typically also have a custom metaclass:

    
                                                                                                                                             
    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
    
    

    Which is fixable by producing a derived metaclass that inherits from the metaclasses in both ancestor branches (this requires that both metaclasses are well behaved, using the super() instead of hardcoding calls to type - but that is the case with well maintained and popular frameworks):

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