Auto Register Subclasses Without Metaclass in Python
In registry pattern, a registry
maintains global association from keys to objects, so that objects can be reached from anywhere by simple identifier. This is useful for doing reverse lookups.
When building a registry, programmers have to explicitly register each object with registry. Manually building a registry is error prone and it is tedious if there are too many objects to register. It is better to auto register objects if possible.
A commonly used approach is to use inheritance as an organizing mechanism. Create a meta class which will auto register classes and then create base class with this meta class.
REGISTRY = {} def register_class(target_class): REGISTRY[target_class.__name__] = target_class class MetaRegistry(type): def __new__(meta, name, bases, class_dict): cls = type.__new__(meta, name, bases, class_dict) if name not in registry: register_class(cls) return cls class BaseClass(metaclass=MetaRegistry): pass class Foo(BaseClass): pass class Bar(BaseClass): pass
Now whenever you subclass BaseClass
, it gets registered in the global registry. In the above example, Foo
, Bar
gets registered automatically.
Eventhough it solves registration problem, it is hard to understand the code unless you know how metaclasses work.
A simple alternative for this is to use __subclasses__()
to get subclasess and register them.
REGISTRY = {cls.__name__: cls for cls in BaseClass.__subclasses__()}
This will work only for direct subclasses and won't with indirect subclasses like this.
class Baz(Bar): pass
To solve this, we can use a function to recursively retrieve all subclasses of a class.
def subclasses(cls, registry=None): if registry is None: registry = set() subs = cls.__subclasses__() for sub in subs: if sub in registry: return registry.add(sub) yield sub for sub in subclasses(sub, registry): yield sub REGISTRY = {cls.__name__: cls for cls in subclasses(BaseClass)}
PEP 487 provides __init_subclass__
hook in class body to customize class creation without the use of metaclass. We can our registration logic in this __init_subclass__
hook.
class BaseClass: def __init_subclass__(cls, **kwargs): if cls not in registry: register_class(cls) super().__init_subclass__(**kwargs) print(registry)
This is available only in Python 3.6+. For older versions, we have to use the recursive function to get all subclasess. This code is easier to understand than metaclass example.
Need further help with this? Feel free to send a message.