类装饰器禁用 __init_subclass__

Posted

技术标签:

【中文标题】类装饰器禁用 __init_subclass__【英文标题】:class decorator disables __init_subclass__ 【发布时间】:2020-01-08 17:32:57 【问题描述】:

我已经四处寻找这个问题的答案,但找不到任何东西。如果之前有人问过这个问题,我深表歉意。

在我知道的 3-4 种方法中,用于从父类对子类强制执行给定方法(编辑元类的 __new__ 方法、挂钩到 builtins.__build_class__、使用 __init_subclass__ 或使用 @987654325 @) 我通常最终使用__init_subclass__,主要是因为易于使用,并且与@abc.abstractmethod 不同,对子类的约束是根据子类定义而不是类实例化来检查的。示例:

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

    def __init__(self):
        pass

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

这个示例代码显然会抛出一个错误,因为Chi 没有foo 方法。尽管如此,我还是偶然发现了这样一个事实,即上游类的这种约束可以通过使用简单的类装饰器来绕过:

def add_hello_world(Cls):
    class NewCls(object):
        def __init__(self, *args, **kwargs):
            self.instance = Cls(*args, **kwargs)

        def hello_world(self):
            print("hello world")

    return NewCls


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

    def __init__(self):
        pass

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

c = Chi()
c.hello_world()

上面的代码运行没有问题。现在,忽略我装饰的类是Par(当然,如果Par 是库代码,我什至可能无法作为用户代码开发人员访问它),我无法真正解释这种行为。对我来说很明显,可以使用装饰器向现有类添加方法或功能,但我从未见过不相关的装饰器(只是打印hello world,甚至不会弄乱类创建)已经禁用方法出现在课堂上。

这是预期的 Python 行为吗?或者这是某种错误?老实说,据我了解,这可能会带来一些安全问题。

这是否只发生在 __init_subclass__ 数据模型上?或者也给其他人?

【问题讨论】:

你从装饰器返回的类从来没有定义你所说的方法,你为什么期望它存在?你已经用一个完全不同的班级替换了你的班级。 我建议阅读about how decorators work;你有一个基本的误解,该帖子应该解决。 【参考方案1】:

请记住,装饰器语法只是函数应用程序:

class Par:
    def __init_subclass__(...):
         ...


Par = add_hello_world(Par)

最初绑定到Par定义__init_subclass__add_hello_world 中定义的 new 类没有,这是后装饰名称 Par 所指的类,也是您要继承的类。


顺便说一句,您仍然可以通过__init__ 访问原始课程Par

显式调用装饰器:

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

    def __init__(self):
        pass

Foo = Par  # Keep this for confirmation

Par = add_hello_world(Par)

我们可以确认闭包保留了对原始类的引用:

>>> Par.__init__.__closure__[0].cell_contents
<class '__main__.Par'>
>>> Par.__init__.__closure__[0].cell_contents is Par
False
>>> Par.__init__.__closure__[0].cell_contents is Foo
True

如果您确实尝试对其进行子类化,您将得到预期的错误:

>>> class Bar(Par.__init__.__closure__[0].cell_contents):
...   pass
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "tmp.py", line 16, in __init_subclass__
    raise AttributeError(f"Must have must_have")
AttributeError: Must have foo

【讨论】:

另请注意:如果您希望新类成为子类,则可以将其设为一个。将class NewCls(object): 更改为class NewCls(Cls): 并删除__init__ 的定义(您不需要instance 属性,因为您将成为Cls/Par 的实际子类的实例,但是装饰者的新班级最好遵守__init_subclass__ 强加的规则)。通常,类装饰器甚至不会这样做,它们会定义要添加的方法,然后分配给Cls,例如Cls.hello_world = hello_world,然后是return Cls;他们修改类,而不是创建一个新类。 非常感谢您的回答。我现在可以清楚地看到它,即使我以前看不到 :)

以上是关于类装饰器禁用 __init_subclass__的主要内容,如果未能解决你的问题,请参考以下文章

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

深入理解 Python 中的 __init_subclass__

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

修补 __init_subclass__

装饰器、装饰器类与类装饰器(三)

flask类视图