1

Context

I started exploring the concept of Metaclass with python. Rapidly, I found myself facing a common problem.

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

From my understanding, this happens when you create a class [C] that inherits from two classes (or more) [A, B] that does not share the same metaclass [M_A, M_B]

M_A     M_B
 :       :
 :       :
 A       B
  \     /
   \   /
     C

The problem is well described here and the solution is simple. We need to create a new metaclass [M_C] that inherits from both M_A and M_B

I tried to make this process automatic by creating a method that creates [M_C] dynamically. Something like this

My problem

My class [C] inherits from [B] and I want to use [M_A] has its metaclass,
M_A is a custom metaclass (singleton)
B's metaclass is abc.ABCMeta

My metaclass_resolver() successfully creates a new metaclass [M_C]
however it inherits from abc.ABCMeta and type instead of inheriting from abc.ABCMeta and M_A.

from collections import UserDict

def metaclass_resolver(*classes):
    
    metaclasses     = tuple(set(type(cls) for cls in classes))
    new_metaclass   = metaclasses[0]
    new_meta_name   = new_metaclass.__name__
    
    #if there's more than one metaclass
    #combine them and create a new metaclass (M_C)
    if len(metaclasses) > 1:
        #get the name of each metaclass
        new_meta_name = "_".join(mcls.__name__ for mcls in metaclasses)
        #create a new dynamic class
        #               type('name','bases {inheritance}, attrs)'
        new_metaclass = type(new_meta_name, metaclasses, {})

    return new_metaclass(new_meta_name, metaclasses, {})

#my custom metaclass (singleton)
class M_A(type):
    def __new__(cls, name, bases, attrs):
        c = super().__new__(cls, name, bases, attrs)
        return c

    def __init__(cls, name, bases, attrs):
        #When this metaclass is initiated, no instance of the class
        #using this metaclass would have been created
        cls.__instance = None
        super().__init__(name, bases, attrs)

    def __call__(cls, *args, **kwargs):
        #get the saved instance
        instance = cls.__instance
        #if the instance does not exists
        if instance is None:
            #create one
            instance = cls.__new__(cls)
            instance.__init__(*args, **kwargs)
            #save it
            cls.__instance = instance

        return instance
    pass

#abc metaclass
class B(UserDict):
    def method_needed_in_C(self):
        pass

#my class
class C(B, metaclass = metaclass_resolver(M_A, B)):
    def __init__(self):
        super().__init__()
        print(type(self.__class__))
        #<class 'abc.ABCMeta_type'>
    pass

if __name__ == "__main__":
    c = C()

In metaclass_resolver() when I use type(cls) it returns <class 'type'> instead of <class 'M_A'> which make sense since every class derived from type. But then, how can I point directly to <class 'M_A'>?

If I use cls directly I get this error : TypeError: descriptor '__init__' requires a 'type' object but received a 'str'

Thanks!

dage023
  • 11
  • 4

1 Answers1

0

You should only use
new_metaclass = type(new_meta_name, metaclasses, {}) and not ever new_metaclass(new_meta_name, metaclasses, {}) <- with this arrangement you are changing the metaclass of the metaclass - and you want the metaclass of the metaclass to always be type if you merely want to combine both metaclasses.

It is likely that on the unexpected input for abc.ABCMeta or even on your own M_A class, yo are getting this side-effect of only having one ancestor metaclass. That is probably the source of your unexpected behaviour.

In other words: to create your metaclass itself, call type not one of the metaclasses you are combining - even it this happen to work, the mechanisms in place that make those useful as "metaclasses" for ordinary classes are probably not useful and certainly outright meaningless for being a metaclass of other combined metaclass which include itself as an ancestor.

Another thing is that by using the tuple(set(...)) thing for eliminating duplicate metaclasses, you are basically randomizing their order: metaclasses are a sensitive thing, and not all of them are built in a way they can be combined with others - and a lot less of them will be able to be combined in a random order. You can use a dict comprehension instead, which will both preserve the order and eliminate duplicates, and can extract the metaclasses name for your fancy naming in one step:

def metaclass_resolver(*classes):
    
    metaclasses     = {mcls.__name__: mcls for cls in classes if (mcls:=type(cls)) is not type}
    new_meta_name = "_".join(metaclasses)
    return type(new_meta_name, tuple(metaclasses.values()), {})
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • Thanks for your response! +1 for introducing me to [the walrus operator](https://docs.python.org/3/whatsnew/3.8.html) However, by using this condition : `if (mcls:=type(cls)) is not type` My custom metaclass [**M_A**] will not be added to the dict since its direct ancestor is `type`. I could create a new class that inherit from my custom metaclass and check if 'type' is in .__bases__ of this new class, but it seems a bit sketchy... – dage023 Jul 10 '21 at 15:02