3.3.3. 自定义类创建

当一个类继承其他类时,那个类的 init_subclass 会被调用。这样就可以编写能够改变子类行为的类。这与类装饰器有紧密的关联,但是类装饰器是影响它们所应用的特定类,而 init_subclass 则只作用于定义了该方法的类所派生的子类。

  • classmethod object.init_subclass(cls)
  • 当所在类派生子类时此方法就会被调用。cls 将指向新的子类。如果定义为一个普通实例方法,此方法将被隐式地转换为类方法。

传入一个新类的关键字参数会被传给父类的 init_subclass。为了与其他使用 init_subclass 的类兼容,应当根据需要去掉部分关键字参数再将其余的传给基类,例如:

  1. class Philosopher:
  2. def __init_subclass__(cls, /, default_name, **kwargs):
  3. super().__init_subclass__(**kwargs)
  4. cls.default_name = default_name
  5.  
  6. class AustralianPhilosopher(Philosopher, default_name="Bruce"):
  7. pass

object.init_subclass 的默认实现什么都不做,只在带任意参数调用时引发一个错误。

注解

元类提示 metaclass 将被其它类型机制消耗掉,并不会被传给 init_subclass 的实现。实际的元类(而非显式的提示)可通过 type(cls) 访问。

3.6 新版功能.

3.3.3.1. 元类

默认情况下,类是使用 type() 来构建的。类体会在一个新的命名空间内执行,类名会被局部绑定到 type(name, bases, namespace) 的结果。

类创建过程可通过在定义行传入 metaclass 关键字参数,或是通过继承一个包含此参数的现有类来进行定制。在以下示例中,MyClassMySubclass 都是 Meta 的实例:

  1. class Meta(type):
  2. pass
  3.  
  4. class MyClass(metaclass=Meta):
  5. pass
  6.  
  7. class MySubclass(MyClass):
  8. pass

在类定义内指定的任何其他关键字参数都会在下面所描述的所有元类操作中进行传递。

当一个类定义被执行时,将发生以下步骤:

  • 解析 MRO 条目;

  • 确定适当的元类;

  • 准备类命名空间;

  • 执行类主体;

  • 创建类对象。

3.3.3.2. 解析 MRO 条目

如果在类定义中出现的基类不是 type 的实例,则使用 mro_entries 方法对其进行搜索,当找到结果时,它会以原始基类元组做参数进行调用。此方法必须返回类的元组以替代此基类被使用。元组可以为空,在此情况下原始基类将被忽略。

参见

PEP 560 - 对 typing 模块和泛型类型的核心支持

3.3.3.3. 确定适当的元类

为一个类定义确定适当的元类是根据以下规则:

  • 如果没有基类且没有显式指定元类,则使用 type()

  • 如果给出一个显式元类而且 不是type() 的实例,则其会被直接用作元类;

  • 如果给出一个 type() 的实例作为显式元类,或是定义了基类,则使用最近派生的元类。

最近派生的元类会从显式指定的元类(如果有)以及所有指定的基类的元类(即 type(cls))中选取。最近派生的元类应为 所有 这些候选元类的一个子类型。如果没有一个候选元类符合该条件,则类定义将失败并抛出 TypeError

3.3.3.4. 准备类命名空间

一旦适当的元类被确定,则类命名空间将会准备好。如果元类具有 prepare 属性,它会以 namespace = metaclass.prepare(name, bases, **kwds) 的形式被调用(其中如果有附加的关键字参数,应来自类定义)。

如果元类没有 prepare 属性,则类命名空间将初始化为一个空的有序映射。

参见

  • PEP 3115 - Python 3000 中的元类
  • 引入 prepare 命名空间钩子

3.3.3.5. 执行类主体

类主体会以(类似于) exec(body, globals(), namespace) 的形式被执行。普通调用与 exec() 的关键区别在于当类定义发生于函数内部时,词法作用域允许类主体(包括任何方法)引用来自当前和外部作用域的名称。

但是,即使当类定义发生于函数内部时,在类内部定义的方法仍然无法看到在类作用域层次上定义的名称。类变量必须通过实例的第一个形参或类方法来访问,或者是通过下一节中描述的隐式词法作用域的 class 引用。

3.3.3.6. 创建类对象

一旦执行类主体完成填充类命名空间,将通过调用 metaclass(name, bases, namespace, **kwds) 创建类对象(此处的附加关键字参数与传入 prepare 的相同)。

如果类主体中有任何方法引用了 classsuper,这个类对象会通过零参数形式的 super(). class 所引用,这是由编译器所创建的隐式闭包引用。这使用零参数形式的 super() 能够正确标识正在基于词法作用域来定义的类,而被用于进行当前调用的类或实例则是基于传递给方法的第一个参数来标识的。

CPython implementation detail: 在 CPython 3.6 及之后的版本中,class 单元会作为类命名空间中的cell is passed to the metaclass as a classcell 条目被传给元类。 如果存在,它必须被向上传播给 type.new 调用,以便能正确地初始化该类。 如果不这样做,在 Python 3.8 中将引发 RuntimeError

当使用默认的元类 type 或者任何最终会调用 type.new 的元类时,以下额外的自定义步骤将在创建类对象之后被发起调用:

  • 首先,type.new 将收集类命名空间中所有定义了 set_name() 方法的描述器;

  • 接下来,所有这些 set_name 方法将使用所定义的类和特定描述器所赋的名称进行调用;

  • 最后,将在新类根据方法解析顺序所确定的直接父类上调用 init_subclass() 钩子。

在类对象创建之后,它会被传给包含在类定义中的类装饰器(如果有的话),得到的对象将作为已定义的类绑定到局部命名空间。

当通过 type.new 创建一个新类时,提供以作为命名空间形参的对象会被复制到一个新的有序映射并丢弃原对象。这个新副本包装于一个只读代理中,后者则成为类对象的 dict 属性。

参见

  • PEP 3135 - 新的超类型
  • 描述隐式的 class 闭包引用

3.3.3.7. 元类的作用

元类的潜在作用非常广泛。已经过尝试的设想包括枚举、日志、接口检查、自动委托、自动特征属性创建、代理、框架以及自动资源锁定/同步等等。