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?

Asked By: Pythonist

||

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
Answered By: jsbueno