元类的“__init_subclass__”方法在由该元类构建的类中不起作用。

4

我的问题是受这个问题启发而来。

问题在于三级类模型——终止类(第三级)应该只存储在注册表中,但第二级正在干扰并且也已经存储了,因为它们是第一级的子类。

我想通过使用元类来摆脱第一级类。通过这种方式,只剩下两个类级别——每个设置组的基类和它们的子类——从相应的基类继承的各种设置类。元类充当类工厂——它应该创建具有所需方法的基类,并且不应在继承树中显示。

但我的想法行不通,因为似乎__init_subclass__方法(链接到方法)没有从元类复制到构造的类中。与我预期的不同的是,__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'>]

我想要在上述模型的子类化过程中添加功能,因此我重新定义了type内置的__init_subclass__如下:

代码片段 № 2.

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

在我看来,现在每一个由Meta_Parent元类(例如,Parent_One)构建的新类都应该有__init_subclass__方法,并且在每个类从这个新类继承时打印出子类的名称,但实际上它没有打印任何东西。也就是说,在发生继承时我的__init_subclass__方法没有被调用。

如果直接继承自Meta_Parent元类,则会起作用:

代码片段 №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__ 就是为了这个目的而创建的。

我一度认为双下划线方法只属于元类,并且不会传递到新构建的类中,但后来尝试了 __init__ 方法,它就像我一开始期望的那样工作 - 看起来每个元类的类都复制了对 __init__ 的链接。

代码片段编号 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. 通过使用元类,实现我的想法是否可行?
2个回答

5

1. 为什么__init__有效,而__init_subclass__无效?

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

  1. The creation of a new class (type) starts in the type_call() function. It does two main things: a new type object creation and this object initialization.

  2. obj = type->tp_new(type, args, kwds); is an object creation. It calls the type's tp_new slot with passed arguments. By default the tp_new stores reference to the basic type object's tp_new slot, but if any ancestor class implements the __new__ method, the reference is changing to the slot_tp_new dispatcher function. Then the type->tp_new(type, args, kwds); callsslot_tp_new function and it, in own turn, invokes the search of __new__ method in the mro chain. The same happens with tp_init.

  3. The subclass initialization happens at the end of new type creation - 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. In my case the object's mro chain has two items:

    print(Parent_One.__mro__)
    ### Output
    (<class '__main__.Parent_One'>, <class 'object'>).
    
  4. int res = type->tp_init(obj, args, kwds); is an object initialization. It also searches the __init__ method in the mro chain, but use the metaclass mro, not the just created new object's mro. In my case the metaclass mro has three item:

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

简化的执行流程图: enter image description here

因此,答案是: 在不同的位置搜索__init_subclass____init__方法:

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

2. 是否可以使用元类来实现我的想法?

我想出了以下解决方案。它有一个缺点——每个子类(包括子孙类)都会调用__init__方法,这意味着所有子类都有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'>}

1
我想到并使用/喜欢的解决方案是:

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


我喜欢这个方法,因为它可以简化子类的创建代码和检查。同时,如果你看到Parent_One,你就知道每当创建一个子类时都会发生一些事情。
我在模拟自己的接口功能(而不是使用ABC)时完成了这个方法,并且override方法检查子类中是否存在某些方法。
人们可以争论override方法是否真的属于元类,还是属于其他地方。

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