如何在定义类时自动注册类

60

我希望在类定义时就注册一个类的实例。理想情况下,下面的代码可以达到这个目的。

registry = {}

def register( cls ):
   registry[cls.__name__] = cls() #problem here
   return cls

@register
class MyClass( Base ):
   def __init__(self):
      super( MyClass, self ).__init__() 

不幸的是,这段代码会生成错误NameError: global name 'MyClass' is not defined

问题出在#problem here这一行,我试图实例化一个MyClass,但装饰器还没有返回,所以它不存在。

有没有什么方法可以使用元类或其他方式解决这个问题?

6个回答

68

是的,元类能够做到这一点。元类的__new__方法返回一个类,所以在返回该类之前只需注册即可。

class MetaClass(type):
    def __new__(cls, clsname, bases, attrs):
        newclass = super(MetaClass, cls).__new__(cls, clsname, bases, attrs)
        register(newclass)  # here is your register function
        return newclass

class MyClass(object):
    __metaclass__ = MetaClass

前面的例子适用于Python 2.x,在Python 3.x中,MyClass的定义略有不同(而MetaClass没有显示,因为它没有改变 - 除非你想要,super(MetaClass, cls)可以变成super()):

#Python 3.x

class MyClass(metaclass=MetaClass):
    pass

从Python 3.6开始,还有一个新的__init_subclass__方法(参见PEP 487),可以用来代替元类(感谢下面@matusko的回答):

class ParentClass:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        register(cls)

class MyClass(ParentClass):
    pass

[编辑:已修复缺失的cls参数到super().__new__()]

[编辑:添加了Python 3.x示例]

[编辑:更正了super()参数顺序,并改进了对3.x差异的描述]

[编辑:添加了Python 3.6 __init_subclass__示例]


3
顺便说一句,这是一个真实世界的代码示例,它完全可以做到这一点(这不是我的代码,但我经常使用这个库)。请查看第67-90行代码(截至我撰写本文时)。https://github.com/ask/celery/blob/master/celery/task/base.py - dappawit
使用元类__new__实际上并没有帮助,只是因为你省略了super调用而看起来有所帮助。请参阅我的答案,获取更详细的解释(可能过于详细)。 - Ben
@Ben:也许我漏掉了什么,但是我没有看到使用元类解决“super”问题的问题所在。我明白删除super调用将解决OP的问题,但我不明白为什么这仍然是我的解决方案中的问题。使用这种方式的元类,在类注册期间永远不会调用“__init__”方法,因为我们注册的是类对象,而不是类实例。因此,在“MyClass.__init__”(和/或其子类)中使用“super()”可以按预期工作。 - dappawit
为什么不在元类的__init __()中注册类实例?该实例在那个时候被创建,您可以简单地将其注册为 register(cls),其中cls__init __()的第一个参数。 - linkyndy
如果父类使用 metaclass=ABCMeta 构建,会怎样呢?我们可以有多个元类吗? - GabrielChu
显示剩余5条评论

57

自从Python 3.6以后,您不需要使用元类来解决这个问题

在Python 3.6中引入了更简单的类创建定制方式(PEP 487)。

__init_subclass__钩子会初始化给定类的所有子类。

提案包括以下子类注册示例。

class PluginBase:
    subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.subclasses.append(cls)
在这个例子中,PluginBase.subclasses将包含整个继承树中所有子类的简单列表。需要注意的是,这也很好地作为混合类使用。

5
这需要子类被实例化吗,还是当子类被定义时就会注册?我尝试使用了这段代码,结果似乎是前者,虽然要求的是后者。 - Ashaman Kingpin
6
它将在子类定义时进行注册。确保导入了包含子类的模块。 - matusko
1
@matusko,不好意思,请问导入哪里了。我已经实现了一个类似的问题,但我的注册表(子类)为空。 - unlockme
如果你在另一个模块(文件)中定义了你的子类,Python 只会在你在程序的某个地方导入该模块后读取定义并注册子类。对于单个模块也是如此,如果在定义子类之前请求 PluginBase.subclasses,则为空。 - matusko

14

问题实际上并不是由您指出的那一行代码引起的,而是由__init__方法中的super调用引起的。如果按照dappawit建议使用元类,则问题仍然存在;该答案中的示例之所以有效,只是因为dappawit通过省略Base类和super调用简化了您的示例。在以下示例中,ClassWithMetaDecoratedClass均无法工作:

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)  # here is your register function
        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之间存在根本性的不兼容。
解决这个问题的一种方法是不使用supersuper解决了一个棘手的问题,但是它引入了其他问题(这是其中之一)。如果您使用复杂的多重继承方案,则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
这是如何工作的:
  1. 我们首先检查cls.__dict__中是否有__init__(而不是它是否具有__init__属性,因为后者始终为真)。如果它从另一个类继承了一个__init__方法,那么我们可能没事(因为超类已经按照通常的方式绑定到其名称),并且如果类使用默认的__init__,则我们不想尝试object.__init__,因此要避免这样做。
  2. 我们查找__init__方法并获取它的func_globals字典,这是全局查找的地方(例如查找在super调用中引用的类)。这通常是定义__init__方法的模块的全局字典。此类字典将在register返回之前插入cls.__name__,因此我们提前自己插入它。
  3. 最后,我们创建一个实例并将其插入注册表。这在try/finally块中完成,以确保无论创建实例是否抛出异常,我们都会删除我们创建的绑定;这非常不太可能是必需的(因为99.999%的时间名称即将被重新绑定),但最好尽可能保持奇怪的魔术相互隔离,以最小化某一天其他奇怪的魔术与其交互时出现问题的几率。
register的这个版本将在作为装饰器或元类调用时都起作用(我仍然认为这不是元类的好用途)。不过,还有一些晦涩的情况可能会失败:
  1. 我可以想象一个奇怪的类,它没有__init__方法,但继承了调用self.someMethod__init__方法,并且someMethod在定义的类中被覆盖并进行了super调用。可能不太可能。
  2. __init__方法可能最初在另一个模块中定义,然后通过在类块中执行__init__ = externally_defined_function来在类中使用。另一模块的func_globals属性意味着我们的临时绑定将破坏该模块中对该类名称的任何定义(糟糕的情况)。同样,不太可能发生。
  3. 可能还有我没有想到的其他奇怪情况。
您可以尝试添加更多的hack来使其在这些情况下更加健壮,但Python的性质是这种hack是可能的,而且不可能使它们绝对防弹。

3

这里的答案在 python3 中对我没用,因为 __metaclass__ 不起作用。

这是我的代码,在定义时注册一个类的所有子类:

registered_models = set()

class RegisteredModel(type):
    def __new__(cls, clsname, superclasses, attributedict):
        newclass = type.__new__(cls, clsname, superclasses, attributedict)
        # condition to prevent base class registration
        if superclasses:
            registered_models.add(newclass)
        return newclass

class CustomDBModel(metaclass=RegisteredModel):
    pass

class BlogpostModel(CustomDBModel):
    pass

class CommentModel(CustomDBModel):
   pass

# prints out {<class '__main__.BlogpostModel'>, <class '__main__.CommentModel'>}
print(registered_models)

在Python 2和3之间,指定元类的语法发生了微小的变化,因此从一种语言转换到另一种语言非常容易。 - martineau

0

直接调用基类应该可以正常工作(而不是使用super()):

  def __init__(self):
        Base.__init__(self)

2
我点赞是因为这实际上(有点)是正确的答案,而且不应该遭受三个踩。虽然没有解释,但如果这个答案得分为0,我也不会投票支持它。 - Ben
1
确实:Base.init(self) 是最简单的解决方案。 - RicLeal
2
这需要解释,花了我一分钟才意识到为什么这很有用。 - c z

0

也可以用类似这样的方式来实现(不需要注册函数)

_registry = {}

class MetaClass(type):
    def __init__(cls, clsname, bases, methods):
        super().__init__(clsname, bases, methods)
        _registry[cls.__name__] = cls


class MyClass1(metaclass=MetaClass): pass
class MyClass2(metaclass=MetaClass): pass

print(_registry)
# {'MyClass1': <class '__main__.MyClass1'>, 'MyClass2': <class '__main__.MyClass2'>}

此外,如果我们需要使用一个基础抽象类(例如Base()类),我们可以这样做(注意元类继承自ABCMeta而不是type)。
from abc import ABCMeta

_registry = {}

class MetaClass(ABCMeta):
    def __init__(cls, clsname, bases, methods):
        super().__init__(clsname, bases, methods)
        _registry[cls.__name__] = cls

class Base(metaclass=MetaClass): pass
class MyClass1(Base): pass
class MyClass2(Base): pass

print(_registry)
# {'Base': <class '__main__.Base'>, 'MyClass1': <class '__main__.MyClass1'>, 'MyClass2': <class '__main__.MyClass2'>}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接