假设我们定义了一个类,但不希望这个类被继承,该怎么做呢?
class NotInheritable: """ 不可被继承的类 """ pass # 希望继承 NotInheritable 的时候报错 # 那么要怎么修改这段逻辑呢? class A(NotInheritable): pass
所以 Python 提供了一个名为 __init_subclass__ 的特殊成员函数,当某个类被继承时,会自动触发。
class NotInheritable: """ 不可被继承的类 """ def __init_subclass__(cls, **kwargs): print(f"被 <class '{cls.__name__}'> 继承了") class A(NotInheritable): pass """ 被 <class 'A'> 继承了 """
在介绍 Python 虚拟机的时候我们说过,如果想创建一个实例对象,那么需要调用类对象;如果想创建一个类对象,那么需要调用 type。
class NotInheritable: """ 不可被继承的类 """ def __init_subclass__(cls, **kwargs): print(f"被 <class '{cls.__name__}'> 继承了") class A(NotInheritable): pass """ 被 <class 'A'> 继承了 """ A = type("A", (NotInheritable,), {}) """ 被 <class 'A'> 继承了 """
代码中的两种创建 class A 的方式是等价的,本质上都是调用 type。而 type 虽然是元类,但它本质上也是个类,调用的时候会执行内部的 __new__ 函数。
而在 __new__ 函数的内部,会检测要创建的类所继承的父类,有没有定义 __init_subclass__。如果有,则执行。
所以我们上面的问题,就可以这么解决。
class NotInheritable: """ 不可被继承的类 """ def __init_subclass__(cls, **kwargs): raise RuntimeError("NotInheritable 不可被继承") class A(NotInheritable): pass # 此时程序会直接报错 """ RuntimeError: NotInheritable 不可被继承 """
此时我们的目的就达到了,让 NotInheritable 无法被继承。但其实 __init_subclass__ 最大的用处是它在一些简单的场景中可以代替元类,来看看它的两个参数。
class Base: def __init_subclass__(cls, **kwargs): """ 注意参数 cls,它表示继承 Base 的类 kwargs 则表示类的一些额外参数 """ print(cls) print(kwargs) class A(Base, name="古明地觉", age=16): pass """ <class '__main__.A'> {'name': '古明地觉', 'age': 16} """
代码打印的结果已经很清晰了,但可能有人会有一个疑问,当第一个参数表示 cls、也就是类的时候,__init_subclass__ 应该被 @classmethod 装饰才对啊。
理论上确实是这样,但实际不需要,而原因我们看一下源码就知道了。我们说创建类的时候,会调用 type.__new__,在源码中对应 type_new 函数,它是类对象创建时的第一案发现场。
源码位于 Objects/typeobject.c 中
所以 __init_subclass__ 其实是被隐式地装饰了,当然我们在代码中也可以手动装饰。如果我们不装饰,那么解释器底层会自动装饰。
其实源码中最上面的注释也写的很清晰了,如果 __init_subclass__ 和 __class_getitem__ 是普通的成员函数,就将它们变成类方法。
class Base: @classmethod def __init_subclass__(cls, **kwargs): print(cls) print(kwargs) class A(Base, say="曾经我也想过一了百了"): @classmethod def __class_getitem__(cls, item): return f"item = {item}" """ <class '__main__.A'> {'say': '曾经我也想过一了百了'} """ # 而 __class_getitem__ 则是让类能够支持索引和切片 print(A["古明地觉"]) print(A[123]) print(A[1: 10: 2]) print(A["x": "y": "z"]) """ item = 古明地觉 item = 123 item = slice(1, 10, 2) item = slice('x', 'y', 'z') """
这两个成员函数可以手动装饰,如果不装饰,那么解释器会自动隐式装饰、或者说帮我们装饰。因此我们在使用的时候,就不需要手动装饰了。
相信 __init_subclass__ 的原理已经很清晰了,然后是它的应用场景,我们说它可以用于元编程。
class Base: def __init_subclass__(cls, **kwargs): # 遍历 kwargs,将 key 作为属性、value 作为值 # 绑定到 cls 里面 for k, v in kwargs.items(): type.__setattr__(cls, k, v) # 小知识:在给对象动态绑定属性时 # 如果是类对象,应该使用 type.__setattr__(类, k, v) # 如果是实例对象,应该使用 object.__setattr__(实例, k, v) # 当然我们也可以绑定一些预定义的属性 # 也就是所有继承该基类的类,都会有的属性 type.__setattr__(cls, "ping", "pong") # 也可以绑定一个成员函数 # 注意函数里面要有一个 self 参数 type.__setattr__(cls, "say_hi", lambda self: "hello world") class A(Base, name="魔理沙", address="魔法森林"): pass print(A.name) # 魔理沙 print(A.address) # 魔法森林 print(A.ping) # pong print(A().say_hi()) # hello world class B(Base): pass print(B.ping) # pong print(B().say_hi()) # hello world
可以看到,我们在不使用元类的情况下,通过 __init_subclass__ 实现了类的自定义过程。当然示例比较简单,你也可以实现更复杂的逻辑,总之在某些场景下我们能用 __init_subclass__ 替代元类。