Tuesday 6 June 2023

Python Metaclasses part 2

Last year I wrote a post about the mysterious world of Python Metaclasses. In that post I just mentioned in passing that the interaction of inheritance and metaclasses adds a bit more of complexity. I've been revisiting this topic and I've thought of writing short complementary post.

One class has just one single metaclass. If that metaclass has not been declared explicitly, e.g. MyClass(metaclass=MyMeta): the metaclass will be taken from the inheritance chain. As most classes do not declare a metaclass, most times the search in the inheritance tree ends in its root, object, that has type as metaclass. The problem with this is that Python has Multiple Inheritance, so what if we have explicit, unrelated metaclasses declared in different branches of the multiple inheritance tree of our class? Well, in that case we get an error:
Exception: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
Let's see an example:


# declare 2 metaclasses, MetaA and MetaB
class MetaA(type):
    def __new__(mcls, name, bases, attrs):
        print(f"{mcls.__name__} {name} - MetaA")
        return super().__new__(mcls, name, bases, attrs)


class MetaB(type):
    def __new__(mcls, name, bases, attrs):
        print(f"{mcls.__name__} {name} - MetaB")
        return super().__new__(mcls, name, bases, attrs)



class A(metaclass=MetaA):
    pass

# MetaA A - MetaA


class B(metaclass=MetaB):
    pass

# MetaB B - MetaB


# try to create a class that inherits from classes that have different, unrelated metaclasses
try:
    class AB(A, B):
        pass
except Exception as ex:
    print(f"Exception: {ex}")
    # MetaA -> type <- MetaB
    
# Exception: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases



If the metaclasses that we obtain from the different branches in the inheritance tree are all in the same inheritance "line" that's fine, we'll get as metaclass the most derived one.


# create another metaclass (it inherits from one of the previous metaclasses)
class MetaAA(MetaA):
    def __new__(mcls, name, bases, attrs):
        print(f"{mcls.__name__} {name} - MetaAA")
        return super().__new__(mcls, name, bases, attrs)


class AA(metaclass=MetaAA):
    pass

# MetaAA AA - MetaAA
# MetaAA AA - MetaA

print(f"type(AA): {type(AA).__name__}")
# type(AA): MetaAA


# inherit from 2 classes that have different metaclasses, but both metaclasses are in the same inheritance "line"
class AAA(AA, A):
    pass

# MetaAA AAA - MetaAA
# MetaAA AAA - MetaA

# it's fine cause AA.metaclass and A.metaclass are in the same inheritance chain
# MetaAA -> MetaA -> type
# it gets as metaclass the most derived one, MetaAA

print(f"type(AAA): {type(AAA).__name__}")
# type(AAA): MetaAA


So long in short, for the metaclasses from the inheritance tree this is fine:
MetaA -> MetaB -> type
but this is not:
MetaA -> type <- MetaB

Notice that one metaclass can inherit from 2 metaclasses. So if you set that derived metaclass as your class metaclass, it's sort of a way of "almost" having 2 metaclasses.



# on the other hand, a metaclass can inherit from several metaclasses
class MetaAB(MetaA, MetaB):
    def __new__(mcls, name, bases, attrs):
        print(f"{mcls.__name__} {name} - MetaAB")
        return super().__new__(mcls, name, bases, attrs)
    
class AB(metaclass=MetaAB):
    pass

# MetaAB AB - MetaAB
# MetaAB AB - MetaA
# MetaAB AB - MetaB


No comments:

Post a Comment