metaclass 的“__init_subclass__”方法在这个 metaclass 构造的 class 中不起作用

The metaclass's "__init_subclass__" method doesn't work in the class constructed by this metaclass

我的问题受到了这个的启发。

问题出在 3 级 class 模型 - 终止 classes(第 3 级)仅应存储在注册表中,但第 2 级正在干扰和也有存储,因为它们是第 1 级的子class。

我想通过使用 metaclass 摆脱第一级 class。通过这种方式,只剩下 2 class 级别 - 每组设置及其子项的基础 classes - 各种设置 classes,继承自相应的基础 class . metaclass 作为一个 class 工厂 - 它应该使用需要的方法创建基础 classes 并且不应该显示在继承树中。

但是我的想法行不通,因为好像__init_subclass__方法(link方法)没有从metaclass复制到构造classes。与 __init__ 方法相反,它按我的预期工作。

代码片段№1.模型的基本框架:

class Meta_Parent(type):
    pass

class Parent_One(metaclass=Meta_Parent):
    pass

class Child_A(Parent_One):
    pass

class Child_B(Parent_One):
    pass

class Child_C(Parent_One):
    pass

print(Parent_One.__subclasses__())

输出:

[<class '__main__.Child_A'>, <class '__main__.Child_B'>, <class '__main__.Child_C'>]

我想为上述模型的 subclassing 过程添加功能,所以我重新定义了 type 的内置函数 __init_subclass__,如下所示:

代码片段 № 2.

class Meta_Parent(type):
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        print(cls)

从我的角度来看,现在每个新的 class,由 Meta_Parent metaclass 构造(例如,Parent_One) 应该有 __init_subclass__ 方法,因此,当每个 class 继承自这个新的 class 时,应该打印 subclass 名称],但它什么也不打印。也就是说,我的 __init_subclass__ 方法在发生继承时不会被调用。

如果 Meta_Parent metaclass 是直接继承的,则有效:

代码片段 № 3.

class Meta_Parent(type):
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        print(cls)

class Child_A(Meta_Parent):
    pass

class Child_B(Meta_Parent):
    pass

class Child_C(Meta_Parent):
    pass

输出:

<class '__main__.Child_A'>
<class '__main__.Child_B'>
<class '__main__.Child_C'>

这里没有什么奇怪的,__init_subclass__ 正是为此目的而创建的。

我当时在想,dunder 方法只属于 metaclass,不会传递给新构造的 classes,但是,我尝试 __init__方法,它像我一开始所期望的那样工作 - 看起来 link 到 __init__ 已经复制到每个 metaclass 的 class.

代码片段 № 4.

class Meta_Parent(type):
    def __init__(cls, name, base, dct):
        super().__init__(name, base, dct)
        print(cls)

输出:

<class '__main__.Parent_One'>
<class '__main__.Child_A'>
<class '__main__.Child_B'>
<class '__main__.Child_C'>

题目:

  1. 为什么 __init__ 有效,但 __init_subclass__ 无效?
  2. 是否可以使用metaclass来实现我的想法?

1。为什么 __init__ 有效,但 __init_subclass__ 无效?

我通过GDB调试CPython找到了答案。

  1. 新 class(类型)的创建从 type_call() 函数开始。它主要做两件事:创建新类型对象和初始化此对象。

  2. obj = type->tp_new(type, args, kwds);是对象创建。它使用传递的参数调用类型的 tp_new 槽。默认情况下,tp_new 存储对 basic type object's tp_new slot 的引用,但如果任何祖先 class 实现了 __new__ 方法,则引用将更改为 slot_tp_new 调度程序函数。然后 type->tp_new(type, args, kwds); 调用 slot_tp_new 函数,它自己调用 mro 链中的 __new__ 方法的搜索。 tp_init.

    也是如此
  3. subclass 初始化发生在新类型创建的末尾 - init_subclass(type, kwds). It searches the __init_subclass__ method in the mro chain of the just created new object by using the super object。在我的例子中,对象的 mro 链有两个项目:

    print(Parent_One.__mro__)
    ### Output
    (<class '__main__.Parent_One'>, <class 'object'>).
    
  4. int res = type->tp_init(obj, args, kwds);是对象初始化。它还在mro链中搜索__init__方法,但使用metaclass mro,而不是刚刚创建的新对象的mro。在我的例子中,metaclass mro 有三个项目:

    print(Meta_Parent.__mro__)
    ###Output
    (<class '__main__.Meta_Parent'>, <class 'type'>, <class 'object'>)
    

简化执行图:

所以,答案是: __init_subclass____init__ 方法在不同的地方搜索:

  • 首先在Parent_One__dict__中搜索__init_subclass__,然后在object__dict__中搜索。
  • __init__的搜索顺序是:Meta_Parent__dict__type__dict__object__dict__.

2。有没有可能通过metaclass?

实现我的想法

我想出了以下解决方案。它有缺点 - __init__ 方法被每个子 class 调用,包括子项,这意味着 - 所有子 class 都有 registry__init_subclass__ 属性,这是不必要的。但它按照我在问题中的要求工作。

#!/usr/bin/python3

class Meta_Parent(type):
    def __init__(cls, name, base, dct, **kwargs):
        super().__init__(name, base, dct)
        # Add the registry attribute to the each new child class.
        # It is not needed in the terminal children though.
        cls.registry = {}
        
        @classmethod
        def __init_subclass__(cls, setting=None, **kwargs):
            super().__init_subclass__(**kwargs)
            cls.registry[setting] = cls

        # Assign the nested classmethod to the "__init_subclass__" attribute
        # of each child class.
        # It isn't needed in the terminal children too.
        # May be there is a way to avoid adding these needless attributes
        # (registry, __init_subclass__) to there. I don't think about it yet.
        cls.__init_subclass__ = __init_subclass__

# Create two base classes.
# All child subclasses will be inherited from them.
class Parent_One(metaclass=Meta_Parent):
    pass

class Parent_Two(metaclass=Meta_Parent):
    pass

### Parent_One's childs
class Child_A(Parent_One, setting='Child_A'):
    pass

class Child_B(Parent_One, setting='Child_B'):
    pass

class Child_C(Parent_One, setting='Child_C'):
    pass

### Parent_Two's childs
class Child_E(Parent_Two, setting='Child_E'):
    pass

class Child_D(Parent_Two, setting='Child_D'):
    pass

# Print results.
print("Parent_One.registry: ", Parent_One.registry)
print("#" * 100, "\n")
print("Parent_Two.registry: ", Parent_Two.registry)

输出

Parent_One.registry:  {'Child_A': <class '__main__.Child_A'>, 'Child_B': <class '__main__.Child_B'>, 'Child_C': <class '__main__.Child_C'>}
#################################################################################################### 

Parent_Two.registry:  {'Child_E': <class '__main__.Child_E'>, 'Child_D': <class '__main__.Child_D'>}

我想出的解决方案use/like是:

class Meta_Parent(type):
    def _init_subclass_override(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        # Do whatever... I raise an exception if something is wrong
        #
        # i.e
        # if sub-class's name does not start with "Child_"
        #     raise NameError
        #
        # cls is the actual class, Child_A in this case

class Parent_One(metaclass=Meta_Parent):
    @classmethod
    def __init_subclass__(cls, **kwargs):
        Meta_Parent._init_subclass_override(cls, **kwargs)


### Parent_One's childs
class Child_A(Parent_One):
    pass

我喜欢这个,因为它干掉了子class的创作code/checks。同时,如果你看到Parent_One,你就知道每当创建子class时就会发生一些事情。

我在模仿我自己的接口功能(而不是使用 ABC)时这样做了,override 方法检查子 classes 中是否存在某些方法。

可以争论覆盖方法是否真的属于元class或其他地方。