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

def*_*ode 39 python oop design-patterns metaclass decorator

我希望在定义类时注册一个类实例.理想情况下,下面的代码可以解决问题.

registry = {}

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

@register
class MyClass( Base ):
   def __init__(self):
      super( MyClass, self ).__init__() 
Run Code Online (Sandbox Code Playgroud)

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

#problem here我正在尝试实例化的一行是什么,MyClass但装饰者还没有返回,所以它不存在.

是不是在使用元类或类似的东西?

dap*_*wit 45

是的,元类可以做到这一点.元类' __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
Run Code Online (Sandbox Code Playgroud)

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

#Python 3.x

class MyClass(metaclass=MetaClass):
    pass
Run Code Online (Sandbox Code Playgroud)

从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
Run Code Online (Sandbox Code Playgroud)

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

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

[编辑:修正了args到super()的顺序,并改进了3.x差异的描述]

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

  • 顺便说一句,这里的代码,不正是这样的现实世界的例子(这不是我的代码,但我已经使用了库很多).看看第67-90行(截至我写这篇文章).https://github.com/ask/celery/blob/master/celery/task/base.py (3认同)

mat*_*sko 17

从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)
Run Code Online (Sandbox Code Playgroud)

在此示例中,PluginBase.subclasses将包含整个继承树中所有子类的普通列表.应该注意的是,这也适用于mixin类.

  • 这是否需要实例化子类,或者是否会在定义子类时注册。我尝试使用该代码,尽管所请求的是后者,但它似乎是前者。 (5认同)
  • 它将在子类定义上注册。确保导入带有子类的模块。 (3认同)

Ben*_*Ben 11

问题实际上并不是由您指定的行引起的,而是由方法中的super调用引起的__init__.如果您使用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)  # 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__()
Run Code Online (Sandbox Code Playgroud)

在这两种情况下问题都是一样的; 在创建类对象register之后,但在绑定到名称之前,调用该函数(由元类或直接作为装饰器).这是gnarly(在Python 2.x中)的地方,因为它要求你引用调用中的类,你只能通过使用全局名称并相信它将被绑定到该名称来合理地执行.调用调用的时间.在这种情况下,这种信任是错误的.supersupersuper

我认为元类是错误的解决方案.元类用于创建具有一些共同的自定义行为的系列,正如类用于创建具有一些共同的自定义行为的实例族.你所做的只是在一个类上调用一个函数.您不会定义一个类来调用字符串上的函数,也不应该定义一个元类来调用类上的函数.

因此,问题是以下几者之间的基本不兼容性:(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
Run Code Online (Sandbox Code Playgroud)

这是如何工作的:

  1. 我们首先检查是否__init__存在cls.__dict__(而不是它是否具有__init__属性,这将始终为真).如果它继承的__init__方法从另一个类,我们很可能罚款(因为超已经以通常的方式绑定到它的名字),我们想说的话不起作用的神奇object.__init__,所以我们要避免试图如果该类使用默认值__init__.
  2. 我们查找__init__方法并获取它的func_globals字典,这是全局查找(例如查找super调用中引用的类)的地方.这通常__init__是最初定义方法的模块的全局字典.这样一本词典是大约cls.__name__只要插入它register的回报,所以我们只是早将其插入自己.
  3. 我们最终创建一个实例并将其插入到注册表中.这是在try/finally块中,以确保我们删除我们创建的绑定,无论创建实例是否抛出异常; 这是不太可能的必要(因为99.999%的时间名称即将反弹),但最好保持像这样的奇怪魔法尽可能绝缘,以尽量减少有一天其他一些奇怪的魔法与之交互不良的可能性它.

register无论是作为装饰器调用还是通过元类调用(我仍然认为它不能很好地使用元类),这个版本都可以工作.有一些不起眼的情况会失败但是:

  1. 我可以想像,一个奇怪的类具有一个__init__方法,但继承了一个调用self.someMethod,并someMethod在课堂上被覆盖被定义,使一个super电话.可能不太可能.
  2. __init__方法可能最初在另一个模块中定义,然后通过__init__ = externally_defined_function在类块中进行在类中使用.func_globals但是,另一个模块的属性,这意味着我们的临时绑定将破坏该模块中该类名称的任何定义(oops).再一次,不太可能
  3. 可能是其他我没有想到的奇怪案例.

在这些情况下,您可以尝试添加更多黑客以使其更加强大,但Python的本质是这些黑客是可能的,并且不可能使它们绝对是防弹.