动态__init_subclass__方法的参数绑定

3

我正在尝试让类装饰器起作用。该装饰器将在应用于的类中添加一个__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'

为什么会出现这种情况,我应该如何操作才能使绑定正常工作?
2个回答

4

__init_subclass__是一个隐式类方法

可能无法使用零参数super(如果想了解原因,请阅读这里),但是应该能够在装饰器本身内显式绑定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吗...? - Rick
嗯,是的 - 从技术上讲,应该是 super(C2, subcls),我有点粗心。当Cls还不存在时,无参super工作的方式(使用__class__单元格)使得这个操作变得棘手,但我认为你可以在装饰器中使用闭包,看看编辑后的效果如何,然后告诉我它的运行情况(未经测试)。 - wim
闭包运行得非常完美。即使使用 exec 和代码字符串构建,它也能正常工作。 - Rick

1

对于那些主张使用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()

这段代码是可以运行的。由于抽象方法已经在Bar中被重写,因此Mai不需要foo方法。另一方面:
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__()

这会导致错误,因为即使中间有一个类有foo方法,Chichi也必须有一个foo方法。


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