views:

62

answers:

2

I want to keep a dictionary of (all, non-immediate included) subclasses in a base class, so that I can instantiate them from a string. I'm doing this because the CLSID is sent through a web form, so I want to restrict the choices to the ones set from the subclasses. (I don't want to eval()/globals() the classname).

class BaseClass(object):
    CLSID = 'base'
    CLASSES = {}

    def from_string(str):
        return CLASSES[str]()

class Foo(BaseClass):
    CLSID = 'foo'
    BaseClass.CLASSES[CLSID] = Foo

class Bar(BaseClass):
    CLSID = 'bar'
    BaseClass.CLASSES[CLSID] = Bar

That obviously doesnt work. But is there something like a @classmethod for init? The idea is that this classmethod would only run once as each class is read and register the class with the baseclass. Something like the following could then work: (Would also save the extra line in Foo and Bar)

class BaseClass(object):
    CLSID = 'base'
    CLASSES = {}

    @classmethod
    def __init__(cls):
        BaseClass.CLASSES[cls.CLSID] = cls 

    def from_string(str):
        return CLASSES[str]()

I thought about using __subclasses__ and then filter() on CLSID, but that only works for immediate subclasses.

So, hoping that I explained my purpose, the question is how to make this work? Or am I going about this in a completely wrong way?

+1  A: 

You could muck with metaclasses to do the job for you, but I think a simpler solution might suffice:

class BaseClass(object):
    CLASS_ID = None
    _CLASSES = {}

    @classmethod
    def create_from_id(cls, class_id):
        return CLASSES[class_id]()

    @classmethod
    def register(cls):
        assert cls.CLASS_ID is not None, "subclass %s must define a CLASS_ID" % cls
        cls._CLASSES[cls.CLASS_ID] = cls

Then to define a subclass just use:

class Foo(BaseClass):
    CLASS_ID = 'foo'

Foo.register()

And finally use the factory method in the BaseClass to create the instances for you:

foo = BaseClass.create_from_id('foo')

In this solution, after the class definition you must call the register class method to register the subclass into the base class. Also, the default CLASS_ID is None to avoid overwriting the base class in the registry if a user forgets to define it.

Bruno Oliveira
+1 for advising me not to 'muck with metaclasses'
Noio
+4  A: 

Irrevocably tying this with the base class:

class AutoRegister(type):
  def __new__(mcs, name, bases, D):
    self = type.__new__(mcs, name, bases, D)
    if "ID" in D:  # only register if has ID attribute directly
      if self.ID in self._by_id:
        raise ValueError("duplicate ID: %r" % self.ID)
      self._by_id[self.ID] = self
    return self

class Base(object):
  __metaclass__ = AutoRegister
  _by_id = {}
  ID = "base"

  @classmethod
  def from_id(cls, id):
    return cls._by_id[id]()

class A(Base):
  ID = "A"

class B(Base):
  ID = "B"

print Base.from_id("A")
print Base.from_id("B")

Or keeping disparate concerns actually separate:

class IDFactory(object):
  def __init__(self):
    self._by_id = {}
  def register(self, cls):
    self._by_id[cls.ID] = cls
    return cls

  def __call__(self, id, *args, **kwds):
    return self._by_id[id](*args, **kwds)
  # could use a from_id function instead, as above

factory = IDFactory()

@factory.register
class Base(object):
  ID = "base"

@factory.register
class A(Base):
  ID = "A"

@factory.register
class B(Base):
  ID = "B"

print factory("A")
print factory("B")

You may have picked up on which one I prefer. Defined separately from the class hierarchy, you can easily extend and modify, such as by registering under two names (using an ID attribute only allows one):

class IDFactory(object):
  def __init__(self):
    self._by_id = {}

  def register(self, cls):
    self._by_id[cls.ID] = cls
    return cls

  def register_as(self, name):
    def wrapper(cls):
      self._by_id[name] = cls
      return cls
    return wrapper

  # ...

@factory.register_as("A")  # doesn't require ID anymore
@factory.register          # can still use ID, even mix and match
@factory.register_as("B")  # imagine we got rid of B,
class A(object):           #  and A fulfills that roll now
  ID = "A"

You can also keep the factory instance "inside" the base while keeping it decoupled:

class IDFactory(object):
  #...

class Base(object):
  factory = IDFactory()

  @classmethod
  def register(cls, subclass):
    if subclass.ID in cls.factory:
      raise ValueError("duplicate ID: %r" % subclass.ID)
    cls.factory[subclass.ID] = subclass
    return subclass

@Base.factory.register  # still completely decoupled
                        # (it's an attribute of Base, but that can be easily
                        # changed without modifying the class A below)
@Base.register  # alternatively more coupled, but possibly desired
class A(Base):
  ID = "A"
Roger Pate
Thank you! The solution with the decorators looks very appealing. I see now why the first option is a bad idea :). I might still include/couple the factory in the base class though, because other 'groups' of classes (inheriting from another base class) have different security restrictions.
Noio