How to “template” a registry class that uses __new__ as a factory?

96 Views Asked by At

I wrote a class BaseRegistry that uses a classmethod as a decorator to register other classes with a string name in a class attribute dictionary registered.

This dictionary is used to return the class associated to the string name given to its __new__ method.

This works quite well but now, I would like to create several Registries of this kind without duplicating code. Of course, if I inherit from BaseRegistry, the dictionary is shared between all subclasses which is not what I want.

I can not figure out how to achieve this “templating”.

Below a code example:

class BaseRegistry:
    registered = {}

    @classmethod
    def add(cls, name):
        def decorator(function):
            cls.registered[name] = function
            return function

        return decorator

    def __new__(cls, name):
        return cls.registered[name]


class RegistryOne(BaseRegistry):
    pass


class RegistryTwo(BaseRegistry):
    pass


@RegistryOne.add("the_one")
class Example1:
    def __init__(self, scale_factor=1):
        self.scale_factor = scale_factor


@RegistryOne.add("the_two")
class Example2:
    def __init__(self, scale_factor=2):
        self.scale_factor = scale_factor


if __name__ == "__main__":
    the_one = RegistryOne("the_one")()
    print(f"{the_one.scale_factor=}")

    assert RegistryOne.registered != RegistryTwo.registered

Of course, I would like to find a solution to make this code works but I am also opened to any alternative implementation of BaseRegistry that would achieve the same goal.

EDIT:

With this implementation, Pylint complains about the line:

the_one = RegistryOne("the_one")()

With this message:

E1102: RegistryOne('the_one') is not callable (not-callable)

3

There are 3 best solutions below

2
blhsing On

One approach would be to define a __init_subclass__ method for the base class to initialize each subclass with its own registered dict:

class BaseRegistry:
    ...

    def __init_subclass__(cls):
        cls.registered = {}

Demo: https://ideone.com/6SaHG3

4
blhsing On

There is no good reason for RegistryOne and RegistryTwo to be subclasses when they really represent instances of the base class.

Make them actual instances instead:

class Registry:
    def __init__(self):
        self.registered = {}

    def add(self, name):
        def decorator(function):
            self.registered[name] = function
            return function
        return decorator

    def __call__(self, name):
        return self.registered[name]

RegistryOne = Registry()
RegistryTwo = Registry()

Demo: https://ideone.com/LPi1Du

0
InSync On

To continue this answer, if you really, really want RegistryOne and RegistryTwo to be classes, then a metaclass can takes care of that:

from functools import partial

class MetaRegistry(type):

  def __new__(mcs, name, bases, namespace):
    # The default thing
    instance = super().__new__(mcs, name, bases, namespace)

    # This creates a class-level variable for each class
    # similar to how normal classes would create such variables for their instances.
    instance.registered = {}

    return instance

  def __call__(cls, name):
    # Instead of creating an instance of `cls` (the default),
    # we look for a registered class and return that.
    # In case you care, this violates the principle of leash astonishment.
    return cls.registered[name]

  # No @classmethod here.
  def add(cls, name, registry = None):
    if registry is None:
      return partial(cls.add, name)

    cls.registered[name] = registry
    return registry

You can then use it as:

class RegistryOne(metaclass = MetaRegistry):
  pass

...or:

class BaseRegistry(metaclass = MetaRegistry):
  pass

class RegistryOne(BaseRegistry):
  pass

class RegistryTwo(BaseRegistry):
  pass

The rest goes exactly as planned:

@RegistryOne.add("the_one")
class Example1:
  ...

@RegistryOne.add("the_two")
class Example2:
  ...
if __name__ == "__main__":
  the_one = RegistryOne("the_one")()
  print(f"{the_one.scale_factor=}")  # the_one.scale_factor=1
    
  assert RegistryOne.registered != RegistryTwo.registered  # fine

Having said that, I concur with @blhsing: This is likely not what you need; consider changing the design instead.