如何从类体中获取当前类的引用?
我想在一个基类中保存一个字典,里面记录所有的子类(包括非直接子类),这样我就可以通过字符串来实例化它们。我这么做是因为CLSID
是通过网页表单发送的,所以我想限制选择的范围,只能是那些从子类中设置的选项。(我不想用eval()
或者globals()
来处理类名)。
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
显然,这样做是不行的。但是有没有类似于@classmethod
的东西可以用在初始化上?我的想法是,这个类方法在每个类被读取时只运行一次,并把这个类注册到基类中。这样的话,下面的代码就可以工作了:(这也能省去在Foo
和Bar
中多写一行代码)
class BaseClass(object):
CLSID = 'base'
CLASSES = {}
@classmethod
def __init__(cls):
BaseClass.CLASSES[cls.CLSID] = cls
def from_string(str):
return CLASSES[str]()
我考虑过使用__subclasses__
,然后在CLSID
上用filter()
,但那样只对直接子类有效。
所以,希望我能解释清楚我的目的,问题是如何让这个工作?或者我是不是走错了方向?
2 个回答
3
你可以尝试使用元类来完成这个任务,但我觉得一个更简单的办法就可以解决问题:
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
然后要定义一个子类,只需使用:
class Foo(BaseClass):
CLASS_ID = 'foo'
Foo.register()
最后,使用基类中的工厂方法来为你创建实例:
foo = BaseClass.create_from_id('foo')
在这个解决方案中,类定义之后,你必须调用注册类的方法来把子类注册到基类中。此外,默认的 CLASS_ID
是 None,这样可以避免如果用户忘记定义它时,覆盖掉注册表中的基类。
7
将这个与基类牢牢绑定在一起:
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")
或者将不同的关注点实际上分开:
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")
你可能已经注意到我更喜欢哪一种。将其与类的层次结构分开定义,你可以很容易地扩展和修改,比如可以用两个名字注册(使用ID属性只能允许一个):
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"
你还可以将工厂实例“放在”基类内部,同时保持它的独立性:
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"