How to register classes that inherit from an abstract class in python
Question:
I’m creating a module with a function that works as a factory of the implemented classes. It works registering the classes with the help of a metaclass (a pattern I have just copied from here).
_registry = {}
def register_class(cls):
_registry[cls.__name__] = cls
class Meta(type):
def __new__(meta, name, bases, class_dict):
cls = type.__new__(meta, name, bases, class_dict)
register_class(cls)
return cls
def factory(name):
return _registry[name]()
This works so far.
Now a particularity I have is that I’m implementing classes that share a lot of functionality, therefore I define an abstract base class that implements most of the shared logic, and a large number of derived classes that refine certain particularities. The problem is that this leads to a metaclass conflict, as the metaclass of the the derived classes is both ABCmeta
and Meta
:
from abc import ABC, abstractmethod
_registry = {}
def register_class(cls):
_registry[cls.__name__] = cls
class Meta(type):
def __new__(meta, name, bases, class_dict):
cls = type.__new__(meta, name, bases, class_dict)
register_class(cls)
return cls
def factory(name):
return _registry[name]()
class Base(ABC):
pass
class Derived1(Base, metaclass=Meta):
pass
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
How can I work around this conflict?
Answers:
Simply combine the metaclasses you need into a suitable, derived, metaclass, and use that one as your metaclass. In this case, jsut derive your metaclass from "ABCMeta" instead of type:
from abc import ABCMeta
class Meta(ABCMeta):
def __new__(meta, name, bases, class_dict):
cls = super().__new__(meta, name, bases, class_dict)
register_class(cls)
return cls
Note the importance of using super().__new__
instead of type.__new__
– that is the requisite for our metaclass to be able to be combined with other metaclasses (as far as none of them interfere directly in the same attributes/logic your own metaclass is working on).
So, if you’d need some of your classes to use ABCMeta, and some that would use your metaclass alon, you could, just by replacing the call to type.__new__
with super().__new__
use your metaclass as a mixin, to combine the ABCMeta as needed:
from abc import ABCMeta
class Meta(type):
...
class MetaWithAbc(Meta, ABCMeta):
pass
class Base(metaclass=MetaWithAbc):
pass
...
Also, since Python 3.6, the need for metaclasses has reduced considerably with the introduction of the special __init_subclass__
method.
To simply add a class to registry, as in this case, there is no need for a custom metaclass, if you have a common baseclass: __init_subclass__
is called once per subclass, as it is created:
from abc import ABC, abstractmethod
_registry = {}
def register_class(cls):
_registry[cls.__name__] = cls
def factory(name):
return _registry[name]()
class Base(ABC):
def __init_subclass__(cls, **kwargs):
# always make it colaborative:
super().__init_subclass__(**kwargs)
register_class(cls)
class Derived1(Base):
pass
I’m creating a module with a function that works as a factory of the implemented classes. It works registering the classes with the help of a metaclass (a pattern I have just copied from here).
_registry = {}
def register_class(cls):
_registry[cls.__name__] = cls
class Meta(type):
def __new__(meta, name, bases, class_dict):
cls = type.__new__(meta, name, bases, class_dict)
register_class(cls)
return cls
def factory(name):
return _registry[name]()
This works so far.
Now a particularity I have is that I’m implementing classes that share a lot of functionality, therefore I define an abstract base class that implements most of the shared logic, and a large number of derived classes that refine certain particularities. The problem is that this leads to a metaclass conflict, as the metaclass of the the derived classes is both ABCmeta
and Meta
:
from abc import ABC, abstractmethod
_registry = {}
def register_class(cls):
_registry[cls.__name__] = cls
class Meta(type):
def __new__(meta, name, bases, class_dict):
cls = type.__new__(meta, name, bases, class_dict)
register_class(cls)
return cls
def factory(name):
return _registry[name]()
class Base(ABC):
pass
class Derived1(Base, metaclass=Meta):
pass
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
How can I work around this conflict?
Simply combine the metaclasses you need into a suitable, derived, metaclass, and use that one as your metaclass. In this case, jsut derive your metaclass from "ABCMeta" instead of type:
from abc import ABCMeta
class Meta(ABCMeta):
def __new__(meta, name, bases, class_dict):
cls = super().__new__(meta, name, bases, class_dict)
register_class(cls)
return cls
Note the importance of using super().__new__
instead of type.__new__
– that is the requisite for our metaclass to be able to be combined with other metaclasses (as far as none of them interfere directly in the same attributes/logic your own metaclass is working on).
So, if you’d need some of your classes to use ABCMeta, and some that would use your metaclass alon, you could, just by replacing the call to type.__new__
with super().__new__
use your metaclass as a mixin, to combine the ABCMeta as needed:
from abc import ABCMeta
class Meta(type):
...
class MetaWithAbc(Meta, ABCMeta):
pass
class Base(metaclass=MetaWithAbc):
pass
...
Also, since Python 3.6, the need for metaclasses has reduced considerably with the introduction of the special __init_subclass__
method.
To simply add a class to registry, as in this case, there is no need for a custom metaclass, if you have a common baseclass: __init_subclass__
is called once per subclass, as it is created:
from abc import ABC, abstractmethod
_registry = {}
def register_class(cls):
_registry[cls.__name__] = cls
def factory(name):
return _registry[name]()
class Base(ABC):
def __init_subclass__(cls, **kwargs):
# always make it colaborative:
super().__init_subclass__(**kwargs)
register_class(cls)
class Derived1(Base):
pass