问题实际上并不是由您指出的那一行代码引起的,而是由__init__
方法中的super
调用引起的。如果按照dappawit建议使用元类,则问题仍然存在;该答案中的示例之所以有效,只是因为dappawit通过省略Base
类和super
调用简化了您的示例。在以下示例中,ClassWithMeta
和DecoratedClass
均无法工作:
registry = {}
def register(cls):
registry[cls.__name__] = cls()
return cls
class MetaClass(type):
def __new__(cls, clsname, bases, attrs):
newclass = super(cls, MetaClass).__new__(cls, clsname, bases, attrs)
register(newclass)
return newclass
class Base(object):
pass
class ClassWithMeta(Base):
__metaclass__ = MetaClass
def __init__(self):
super(ClassWithMeta, self).__init__()
@register
class DecoratedClass(Base):
def __init__(self):
super(DecoratedClass, self).__init__()
问题在两种情况下都相同;即在创建
类对象之后但在其绑定到名称之前调用
register
函数(由元类或直接作为装饰器)。这就是在Python 2.x中
super
变得艰难的地方,因为它要求您在
super
调用中引用该类,而您只能通过使用全局名称来合理地完成这一点,并信任它将在调用
super
时绑定到该名称。在这种情况下,这种信任是错误的。
我认为在这里使用元类是错误的解决方案。元类用于创建具有某些通用行为的
类族,正如类用于创建具有某些通用行为的
实例族一样。你只是在对一个类调用函数。您不会定义一个类来对一个字符串调用函数,也不应该定义一个元类来对一个类调用函数。
因此,问题在于:(1)在类创建过程中使用钩子来创建类的实例,和(2)使用
super
之间存在根本性的不兼容。
解决这个问题的一种方法是不使用
super
。
super
解决了一个棘手的问题,但是它引入了其他问题(这是其中之一)。如果您使用复杂的多重继承方案,则
super
的问题比不使用
super
的问题更好,如果您从使用
super
的第三方类继承,则必须使用
super
。如果这两个条件都不成立,那么将您的
super
调用替换为直接基类调用可能实际上是一个合理的解决方案。
另一种方法是不要将
register
钩入类创建中。在每个类定义后添加
register(MyClass)
与添加
@register
或将
__metaclass__ = Registered
(或任何您称之为元类)放入其中相当。底部的一行远不如顶部的漂亮声明易于自我记录,因此这并不是很好,但是这实际上可能是一个合理的解决方案。
最后,您可以使用令人不愉快但可能有效的黑客。问题在于,在名称绑定到模块全局范围之前,正在查找该名称。因此,您可以使用以下方法欺骗:
def register(cls):
name = cls.__name__
force_bound = False
if '__init__' in cls.__dict__:
cls.__init__.func_globals[name] = cls
force_bound = True
try:
registry[name] = cls()
finally:
if force_bound:
del cls.__init__.func_globals[name]
return cls
这是如何工作的:
- 我们首先检查
cls.__dict__
中是否有__init__
(而不是它是否具有__init__
属性,因为后者始终为真)。如果它从另一个类继承了一个__init__
方法,那么我们可能没事(因为超类已经按照通常的方式绑定到其名称),并且如果类使用默认的__init__
,则我们不想尝试object.__init__,因此要避免这样做。
- 我们查找
__init__
方法并获取它的func_globals
字典,这是全局查找的地方(例如查找在super
调用中引用的类)。这通常是定义__init__
方法的模块的全局字典。此类字典将在register
返回之前插入cls.__name__
,因此我们提前自己插入它。
- 最后,我们创建一个实例并将其插入注册表。这在try/finally块中完成,以确保无论创建实例是否抛出异常,我们都会删除我们创建的绑定;这非常不太可能是必需的(因为99.999%的时间名称即将被重新绑定),但最好尽可能保持奇怪的魔术相互隔离,以最小化某一天其他奇怪的魔术与其交互时出现问题的几率。
register
的这个版本将在作为装饰器或元类调用时都起作用(我仍然认为这不是元类的好用途)。不过,还有一些晦涩的情况可能会失败:
- 我可以想象一个奇怪的类,它没有
__init__
方法,但继承了调用self.someMethod
的__init__
方法,并且someMethod
在定义的类中被覆盖并进行了super
调用。可能不太可能。
__init__
方法可能最初在另一个模块中定义,然后通过在类块中执行__init__ = externally_defined_function
来在类中使用。另一模块的func_globals
属性意味着我们的临时绑定将破坏该模块中对该类名称的任何定义(糟糕的情况)。同样,不太可能发生。
- 可能还有我没有想到的其他奇怪情况。
您可以尝试添加更多的hack来使其在这些情况下更加健壮,但Python的性质是这种hack是可能的,而且不可能使它们绝对防弹。
__new__
实际上并没有帮助,只是因为你省略了super
调用而看起来有所帮助。请参阅我的答案,获取更详细的解释(可能过于详细)。 - Ben__init __()
中注册类实例?该实例在那个时候被创建,您可以简单地将其注册为register(cls)
,其中cls
是__init __()
的第一个参数。 - linkyndymetaclass=ABCMeta
构建,会怎样呢?我们可以有多个元类吗? - GabrielChu