How to get a reference to the current class from class body?
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 w开发者_如何学Gorong way?
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"
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.
精彩评论