动态 __init_subclass__ 方法的参数绑定

Posted

技术标签:

【中文标题】动态 __init_subclass__ 方法的参数绑定【英文标题】:argument binding for dynamic __init_subclass__ method 【发布时间】:2018-10-12 17:12:22 【问题描述】:

我正在尝试让类装饰器工作。装饰器会将__init_subclass__ 方法添加到它所应用到的类中。

但是,当方法被动态添加到类时,第一个参数不会绑定到子类对象。为什么会这样?

作为一个例子:这是可行的,下面的静态代码是我试图最终得到的一个例子:

class C1:
    def __init_subclass__(subcls, *args, **kwargs):
        super().__init_subclass__(*args, **kwargs)
        print(f"init_subclass -> subcls.__name__, args!r, kwargs!r")

测试:

>>> D = type("D", (C1,), )
init_subclass -> D, (), 

但是,如果我动态添加 __init__subclass__ 方法,则子类不会绑定到第一个参数:

def init_subclass(subcls, **kwargs):
    super().__init_subclass__(**kwargs)
    print(f"init_subclass -> subcls.__name__, args!r, kwargs!r")

def decorator(Cls):
    Cls.__init_subclass__ = init_subclass
    return Cls

@decorator
class C2:
    pass

测试:

>>> D = type("D", (C2,), )
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: init_subclass() missing 1 required positional argument: 'subcls'

为什么会发生这种情况,我怎样才能做到这一点并让绑定以正确的方式工作?

【问题讨论】:

【参考方案1】:

__init_subclass__ is an implicit classmethod.

可能无法使用零参数 super(如果您想了解原因,请阅读 here),但您应该能够在装饰器本身内显式绑定 super。

def decorator(Cls):
    def __init_subclass__(subcls, **kwargs):
        print(f'init subclass Cls!r, subcls!r, kwargs!r')
        super(Cls, subcls).__init_subclass__(**kwargs)
    Cls.__init_subclass__ = classmethod(__init_subclass__)
    return Cls

@decorator
class C:
    pass

class D(C):
    pass

【讨论】:

我想我有一个想法,但是你能解释一下解释器对super()subcls 参数的需求吗?没有它,我会收到运行时错误。参数不应该是C2...吗? 嗯,是的——从技术上讲,它应该是super(C2, subcls),我很草率。当 Cls 尚不存在时,无参数 super 的工作方式(使用 __class__ 单元格)使得这很棘手,但我认为您可以在装饰器中使用闭包,请参阅编辑并让我知道它是如何实现的去(未经测试)。 关闭效果很好。它甚至可以在使用exec 和一串代码构建时工作。【参考方案2】:

只是对那些提倡使用abc 的人的评论。虽然abc 也可以解决相关问题,但值得一提的是两种方法之间存在两个差异(据我所知):

类定义与实例化。

abc.abstractmethod 装饰器在类实例化时强制对子类进行约束,而 __init_subclass__ 数据模型在类定义中已经这样做了。示例:

class Foo(abc.ABC):  
    def init(self):
        pass
    @abc.abstractmethod
    def foo():
        pass

class Bar(Foo):
    pass

此代码将毫无问题地编译。当您通过例如调用子类的构造函数时,错误将首先出现x = Bar()。如果这是库代码,这意味着错误直到运行时才会出现。另一方面,以下代码:

class Par():
    def __init_subclass__(cls, *args, **kwargs):
        must_have = 'foo'
        if must_have not in list(cls.__dict__.keys()):
            raise AttributeError(f"Must have must_have")
    def __init__(self):
        pass

class Chi(Par):
    def __init__(self):
        super().__init__()

会抛出错误,因为检查是在类定义时执行的。

通过继承级别强制执行

另一个区别是abstractmethod希望被修饰的方法被覆盖一次,但__init_subclass__数据模型也会对子类的子类强制执行约束。示例:

class Foo(abc.ABC):
    def __init__(self):
        pass

    @abc.abstractmethod
    def foo():
        pass

class Bar(Foo):
    def __init__(self):
        super().__init__()
    def foo(self):
        pass

class Mai(Bar):
    pass

x = Mai()

此代码将起作用。 Mai 不需要 foo 方法,因为抽象方法已经在 Bar 中被覆盖。另一方面:

class Par():
    def __init_subclass__(cls, *args, **kwargs):
        must_have = 'foo'
        if must_have not in list(cls.__dict__.keys()):
            raise AttributeError(f"Must have must_have")
    def __init__(self):
        pass

class Chi(Par):
    def __init__(self):
        super().__init__()

    def foo(self):
        pass

class Chichi(Chi):
    def __init__(self):
        super().__init__()

这将引发错误,因为Chichi ALSO 必须有一个 foo 方法,即使两者之间的类有一个。

【讨论】:

以上是关于动态 __init_subclass__ 方法的参数绑定的主要内容,如果未能解决你的问题,请参考以下文章

Python:动态创建类,同时为 __init__subclass__() 提供参数

为啥`type.__new__`调用`__init_subclass__`?

深入理解 Python 中的 __init_subclass__

元类的“__init_subclass__”方法在此元类构造的类中不起作用

修补 __init_subclass__

如何从 __init__ 访问 __init_subclass__ 中的变量?