Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In which circumstances multiple metaclasses from the parent classes are invoked?

The strangely behaving code (tested with Python 2.7.3):

class Meta1(type):
   def __new__(mcl, name, bases, attrs):
        print "Hello Meta1.__new__ "
        return super(Meta1, mcl).__new__(mcl, name, bases, attrs)

class Meta2(type):
    def __new__(mcl, name, bases, attrs):
        print "Hello Meta2.__new__ "
        return super(Meta2, mcl).__new__(
            type, # looks to cause all strange behavior, but anyway pass type here, not mcl
            name, bases, attrs)

print "Declaring BaseClass1"
class BaseClass1(object):
    __metaclass__ = Meta1

print "-----------------------"
print "Declaring BaseClass2"
class BaseClass2(BaseClass1):
    __metaclass__ = Meta2

print "-----------------------"
print BaseClass2.__class__

Its' output:

Declaring BaseClass1
Hello Meta1.__new__ 
-----------------------
Declaring BaseClass2
Hello Meta2.__new__ 
Hello Meta1.__new__ # WHY WAS IT INVOKED?
-----------------------
<class '__main__.Meta1'>

Questions about the code:

Why the class BaseClass2 is defined without any issues even though the __metaclass__ attribute for BaseClass2 is set to Meta2 and for its' parent class BaseClass1 the __metaclass__ attribute is set to Meta1, and neither Meta1 no Meta2 is a subclass of the another class?

Why at the BaseClass2 definition both Meta2.__new__ and Meta1.__new__ are called?

In which circumstances methods in the metaclasses of the parent classes are invoked?

Long story:

While trying to understand how metaclasses in our project work I crafted the code which can be found above. (The project uses Python 2.7.3, and it looks that the metaclasses use in the project is sound since they are used for providing API to the users and metaclasses do quite a lot of things for the user under the hood.)

In the first place I was trying to find the documentation on how the metaclasses work with inheritance. The following (quite old but looks to be valid for Python 2.7) article by Guido van Rossum shed some light on how metaclass is picked in the case of inheritance, what are the requirements to the metaclass of the subling class and minor tricks which can be performed by Python when choosing the metaclass for the sibling class: https://www.python.org/download/releases/2.2.3/descrintro/. This and other writings which I have read on the metaclasses in Python don't explain the behavior I am observing. I guess reading Python interpreter code will shed the light but I believe in the power of documentation and hope that this extreme measure can be avoided. Any answers/pointers to the materials which describe the code behavior observed are welcome.

like image 918
Pavel Shishpor Avatar asked Nov 09 '22 07:11

Pavel Shishpor


1 Answers

After a lot of looking around, I think I found the answer. The Python 3 documentation has one section that says this.

3.3.3.3. Determining the appropriate metaclass

The appropriate metaclass for a class definition is determined as follows:

  • if no bases and no explicit metaclass are given, then type() is used
  • if an explicit metaclass is given and it is not an instance of type(), then it is used directly as the metaclass
  • if an instance of type() is given as the explicit metaclass, or bases are defined, then the most derived metaclass is used

The most derived metaclass is selected from the explicitly specified metaclass (if any) and the metaclasses (i.e. type(cls)) of all specified base classes. The most derived metaclass is one which is a subtype of all of these candidate metaclasses. If none of the candidate metaclasses meets that criterion, then the class definition will fail with TypeError.

I think this still also applies to Python 2 (v2.7 anyway) even though I can't find anything like the above in its documentation.

The reason why the BaseClass2 definition invokes both Meta2.__new__() and Meta1.__new__() is simple — Meta2.__new__() explicity invokes it via its call to super(). However to get it to work properly, you'd also need to change Meta2.__new__() so it returns super(Meta2, mcl).__new__(mcl, name, bases, attrs) instead of super(Meta2, mcl).__new__(type, name, bases, attrs).

like image 151
martineau Avatar answered Dec 04 '22 11:12

martineau