Motivation
In an ideal world of OOP classes should behave like building blocks–put two of them together, and as long as they are shaped to fit each other, you get something larger with functions of both.
While that’s generally true with Python’s multiple inheritance, in which developers are entrusted to design base classes that can work well with others (by calling super()
and dealing with the returning value appropriately, for example), it is unfortunately not the case with base classes that involve multiple metaclasses, where Python would treat developers with kid gloves, eager to declare “metaclass conflict”, even when the developers know full well that there is no actual conflict among the metaclasses involved, and even when those metaclasses are well written to work with others.
The solution to a metaclass conflict is well known, by creating a new metaclass that inherits from all the metaclasses involved. Some have even created recipes (such as this and that) to automate the monotomous metaclass merging process.
But to me, all of the workarounds and resulting code clutters would’ve been unnecessary had Python not been so eager to declare “metaclass conflict” prematurely in the first place, or had Python built a conflict resolution mechanism internally. To me, the only legitimate “metaclass conflict” that Python should lay a ground rule on is for metaclasses with different custom namespaces prepared. That’s it. All the other usages of multiple metaclasses should work just like multiple inheritance does, where developers are expected to take the responsibility of making classes cooperative.
This would solve the problem in a more generic fashion than my other recent proposal to Make abc.ABC a regular class by making __instancecheck__
and __subclasscheck__
class methods, which aimed only at a specific (although the biggest) use case of abc.ABC
.
The Proposal
Python should automatically create a merged metaclass with which to create a new class when there are multiple non-type
metaclasses in the base classes in the class definition.
Prepared namespaces are merged in the order of MRO, and Python should raise a TypeError
only if there are multiple non-dict
types of namespaces prepared by the metaclasses involved.
The implementation of the automatic metaclass merging process should be something logically similar to (somewhat adapted from this recipe):
class CombineMeta:
def __prepare__(self, name, bases, **kwargs):
namespace = {}
for metaclass in self._get_most_derived_metaclasses(bases):
ns = metaclass.__prepare__(name, bases, **kwargs)
if type(ns) in (dict, type(namespace)):
namespace.update(ns)
else:
if type(namespace) is not dict:
raise TypeError('metaclass conflict: '
'multiple custom namespaces defined.')
ns.update(namespace)
namespace = ns
return namespace
def __call__(self, name, bases, namespace, **kwargs):
metaclasses = self._get_most_derived_metaclasses(bases)
if len(metaclasses) > 1:
merged_name = '__'.join(meta.__name__ for meta in metaclasses)
ns = self.__prepare__(merged_name, metaclasses)
metaclass = self(merged_name, tuple(metaclasses), ns, **kwargs)
else:
metaclass, = metaclasses or (type,)
return metaclass(name, bases, namespace, **kwargs)
@staticmethod
def _get_most_derived_metaclasses(bases):
metaclasses = []
for metaclass in map(type, bases):
if metaclass is not type:
metaclasses = [other for other in metaclasses
if not issubclass(metaclass, other)]
if not any(issubclass(other, metaclass)
for other in metaclasses):
metaclasses.append(metaclass)
return metaclasses
combine_meta = CombineMeta()
so that:
from abc import ABC
from enum import Enum
class Foo(ABC, Enum, metaclass=combine_meta):
BAR = 1
Foo.register(int)
print(issubclass(int, Foo)) # outputs True
print(Foo.BAR.value) # outputs 1
print(type(Foo)) # outputs <class '__main__.ABCMeta_EnumType'>
But with the mechanism above built-in to the language, one can then simply write:
class Foo(ABC, Enum):
BAR = 1
and the code would work out of the box like putting together building blocks.
Some purists may find the implict name of the merged metaclass irking, but there’s precedent in implicit attribute names created through name mangling. As long as the behavior is documented I don’t think it’s a problem.
Backward Compatibility
There should be no backward compatibility issues because inheriting from multiple classes with distinct metaclasses currently produces a TypeError
.
Performance Impact
The automatic metaclass merging process will incur a small overhead in creation of classes with base classes. Since class creation usually occurs only during module loading, the impact to overall performance should be minimal.