The library I'm writing makes heavy use of metaclasses. As an example, here is a basic singleton implementation:
class SingletonMeta(type):
_instance = None
def __call__(self, *args, **kwargs):
if self._instance is None:
self._instance = super().__call__(*args, **kwargs)
return self._instance
class ExampleSingleton(metaclass=SingletonMeta):
pass
This works perfectly fine, but problems arise when multiple inheritance is used and the other class also has a metaclass. Metaclasses are fairly common in the standard library; the most notable is abc.ABCMeta
. A naive attempt to make an abstract singleton fails:
class AbstractSingleton(ExampleSingleton, abc.ABC):
pass
Traceback (most recent call last):
File "untitled.py", line 25, in <module>
class AbstractSingleton(ExampleSingleton, abc.ABC):
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
The workaround is easy enough - create a new metaclass that inherits from SingletonMeta
and ABCMeta
- but it's really annoying for anyone who wants to use my library.
class AbstractSingletonMeta(SingletonMeta, abc.ABCMeta):
pass
class AbstractSingleton(metaclass=AbstractSingletonMeta):
pass
# no metaclass conflict
What's the best way to deal with this problem?
Some of my ideas:
SingletonMeta
a subclass of ABCMeta
.AbstractSingletonMeta
in my library for the user's convenience.class AbstractSingleton(ExampleSingleton, abc.ABC, metaclass=auto_merge_metaclasses):
)Since all the information about the needed metaclasses and what needs to be combined is already present in the base classes, and those are passed to the metaclass call, it is possible to have a callable that will inspect all the bases and their used metaclasses, and dynamically create a combining metaclass.
The types
module have some callables that otherwise make it easy to pick the correct metaclass for a set of base classes. So, the function bellow should suffice to your needs, if all your used metaclasses are combinable in an arbitrary order:
from types import prepare_class
def combine_meta(name, bases, namespace, **kwargs):
metaclasses = {prepare_class(name, (base,))[0] for base in bases}
metaclasses.discard(type)
if len(metaclasses) > 1:
meta_name = '_'.join(mcs.__name__ for mcs in metaclasses)
metaclass = combine_meta(meta_name, tuple(metaclasses), {})
elif len(metaclasses) == 1:
metaclass = metaclasses.pop()
else:
metaclass = type
return metaclass(name, bases, namespace, **kwargs)
I've tested this with this sequence in the interactive interpreter, and it worked that far:
class M1(type): pass
class M2(type): pass
class A(metaclass=M1): pass
class B(metaclass=M2): pass
class C(A, B): pass # This raises a metaclassconflict
class C(A, B, metaclass=combine_meta): pass