高天拆解Python元类机制 标定定义期构建与实例化拦截规则

【python】metaclass理解加入门,看完就知道什么是元类了。 — 码农高天

码农高天

元类在类定义阶段通过__new__与__init__完成属性约束与类对象构建,并在调用类生成实例时由__call__统一接管创建逻辑。

AI-assisted summary of 【python】metaclass理解加入门,看完就知道什么是元类了。 — original source. Check the original for full context.

Source published:

Key ideas

  • 元类(Metaclass)是类的“制造者”,声明 `class A(metaclass=M)` 并非继承,而是指示 Python 使用 `M` 代替默认的 `type` 来构建该类对象。 (0:00)
  • 元类的 `__new__` 与 `__init__` 在类定义阶段立即执行,可用于对类属性进行强行校验、拦截或动态修改,解决普通继承无法覆盖的元编程场景。 (0:00)
  • 元类的 `__call__` 函数在类实例化(如 `A()`)时触发,能直接控制对象的产生过程,是实现无侵入式单例模式的核心机制。 (5:00)
  • 在元类中通过 `super()` 调用父类方法存在参数陷阱:`__init__` 与 `__call__` 需剔除显式的 `self`/`cls` 参数,而 `__new__` 则必须保留。 (5:00)

Detailed notes

## Background 该视频由 YouTube 频道“码农高天”于 2024 年 1 月 25 日发布。简介指出,由于元类(metaclass)本身概念偏晦涩,本期视频主要从使用者角度带大家入门、理解和认识元类的基本用法,未来有机会再深入介绍其底层机制。

## TL;DR 视频从 Python 中动态生成类的底层逻辑出发,系统阐述了元类的本质是用来替代默认的 type 来构建类对象的机制。高天深入梳理了元类的核心生命周期方法:__new__ 与 __init__ 在类定义声明阶段即被隐式触发,可用于在类构建期进行属性拦截与约束检查;而元类的 __call__ 则在类被实例化时触发,常用于实现单例模式等控制实例生成的逻辑。此外,视频还特别指出了在元类中使用 super() 调用时 __new__ 与 __init__、__call__ 传参规范不一致的常见开发陷阱。

## Atmosphere & style 视频风格务实且通俗易懂,高天以标志性的清晰条理和循序渐进的代码实验,将原本高深抽象的 Python 元编程机制拆解得直观透彻。整个讲解兼具启发性与警示性,既有揭开底层黑盒时豁然开朗的满足感,又在提醒 super() 传参大坑时展现出资深开发者的严谨与敏锐。

## Bulletin - [00:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=0s) 元类(Metaclass)是类的“制造者”,声明 `class A(metaclass=M)` 并非继承,而是指示 Python 使用 `M` 代替默认的 `type` 来构建该类对象。 - [00:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=0s) 元类的 `__new__` 与 `__init__` 在类定义阶段立即执行,可用于对类属性进行强行校验、拦截或动态修改,解决普通继承无法覆盖的元编程场景。 - [05:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=300s) 元类的 `__call__` 函数在类实例化(如 `A()`)时触发,能直接控制对象的产生过程,是实现无侵入式单例模式的核心机制。 - [05:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=300s) 在元类中通过 `super()` 调用父类方法存在参数陷阱:`__init__` 与 `__call__` 需剔除显式的 `self`/`cls` 参数,而 `__new__` 则必须保留。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=0s) Python 中“一切皆对象”,类本身也是一个对象,默认情况下静态类定义等价于调用 `type(name, bases, attrs)` 动态创建类。 - [00:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=0s) 通过在类声明中指定 `metaclass=M`,可替代底层直通 C 函数的默认 `type` 行为,将类对象变为元类 `M` 的实例。 - [00:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=0s) 元类的 `__new__` 方法在 Python 解析并定义类时即刻触发,无需创建任何类的实例即可执行。 - [00:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=0s) 借助元类的 `__new__` 可以在类定义时对类属性字典进行审查,例如通过检测方法名前缀来强制推行架构规范,不合规则即刻抛错中断加载。 - [05:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=300s) 元类的 `__init__` 紧随其 `__new__` 之后在类定义阶段执行,此时类对象已经构建完毕,适合对其直接进行属性扩展与加工。 - [05:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=300s) 极易混淆的元类 `__call__` 并不在类定义时运行,而是在类被调用以创建实例(如执行 `o = A()`)时触发。 - [05:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=300s) 元类 `__call__` 的典型应用场景是单例模式(Singleton),通过拦截实例创建流程返回缓存对象,避免污染业务类内部逻辑。 - [05:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=300s) 在元类中使用 `super()` 语法时需注意传参差异:`__new__` 必须保留首位的类参数,而 `__init__` 与 `__call__` 则需要将其去除。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=0s) 元类(Metaclass)的基本概念与调用时机

元类(Metaclass)在 Python 中是一个公认比较晦涩且涉及知识点极多的话题。从使用者的角度来看,我们可以通过对比静态类定义与动态类生成来理解它。通常情况下,我们静态定义一个 `class A:`,其底层本质与调用 `type(name, bases, attrs)` 动态生成类是完全等价的,都是向 `type` 传入类名、父类元组以及属性字典这三个参数。由于直接使用 `type` 会直接下沉调用到底层的 C 函数中,如果我们希望在类创建的中间环节插入自定义逻辑,就需要借助元类来实现类似 `A = M("A", bases, attrs)` 的效果。

当我们在类定义中写下 `class A(metaclass=M):` 时,并不代表 `A` 继承了 `M`(继承的语法是 `class A(M):`),而是向 Python 明确指示:“不要再用默认的 `type` 来给我建立类了,请使用 `M` 来给我建立这个类。”在 Python 中,“一切皆对象”,类本身也是一个对象。此时,类 `A` 是元类 `M` 的一个实例对象(instance),而后续通过 `o = A()` 实例化出的 `o` 则是类 `A` 的对象。

我们在执行 `o = A()` 时会隐式调用类 `A` 的 `__new__` 和 `__init__` 函数;同理,当定义 `class A(metaclass=M):` 时,Python 也会在幕后隐式调用元类 `M` 的 `__new__` 和 `__init__` 函数,并传入相应的三个参数。高天通过在 `class M(type)` 的 `__new__` 方法中加入打印 `name`、`bases` 和 `dict` 的语句来验证这一点:代码中仅仅声明了 `class A`,并没有使用 `A` 创建任何实例,终端就已经打印出了对应的参数信息。这证明了元类的 `__new__` 函数是在“定义类”那一瞬间被执行的,开发者可以借此在类构建阶段插入自定义逻辑。

“在 class A 的 definition 里面加了一个 metaclass 等于 M 的意思就是,你不要再用 type 来给我建立 class 了,你用这个 M 来给我建立 class。”

```mermaid flowchart TD accTitle: Python元类与类及实例的实例化关系 accDescr: 展示元类M、类A与实例o之间的两层实例化机制及调用方法

subgraph MetaclassLevel["元类层 (Metaclass)"] M["元类 M (继承自 type)"] end

subgraph ClassLevel["类定义阶段 (Class Definition)"] A["类 A<br/>class A(metaclass=M)"] end

subgraph InstanceLevel["实例阶段 (Instantiation)"] O["实例 o<br/>o = A()"] end

M -->|"类定义时隐式调用<br/>M.__new__ / M.__init__"| A A -->|"实例化时隐式调用<br/>M.__call__ -> A.__new__ / A.__init__"| O ```

- 元类是用来解决普通类继承(Inheritance)所无法解决的问题的机制。 - 适用场景示例:对类的属性或方法名进行强约束。比如规定类中不能出现以 `test_` 开头的函数,可以在元类的 `__new__` 中检查 `dict` 的所有键,若匹配到则直接 `raise Error`,这样在类定义的阶段就会直接报错拦截。 - 元类赋予了开发者在现有继承体系之外,根据成员定义动态增删或修改类属性与方法的自由度。

[05:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=300s) 元类的 `__init__` 与 `__call__` 函数及避坑指南

与 Python 中的其他对象一样,元类 `M` 同样可以拥有 `__init__` 函数。元类的 `__init__` 也发生在类定义被加载的时候,紧随 `__new__` 之后执行。二者最大的区别在于:当 `__init__` 执行时,代表类本身的那个对象(即 `A`,它是元类 `M` 的一个实例)已经构建完成了。此时可以直接对新建的类对象进行加工,例如在 `M` 的 `__init__` 中为 `self.random_id` 赋予一个 0 到 100 的随机数,之后访问 `A.random_id` 便能得到该值。虽然简单的属性赋值可以直接在类体中书写,但在面对需要元编程的复杂动态逻辑时,元类就显得不可或缺。

除了 `__new__` 和 `__init__`,元类中还可以重载 `__call__` 函数,这也是极易被混淆的地方。元类的 `__call__` 并不是在定义 `class A` 时执行的,而是在执行 `o = A()` 时被触发的。换言之,元类的 `__call__` 接管了该类尝试产生实例的过程。通常情况下,我们在元类的 `__call__` 中会直接调用父类(`type`)的 `__call__` 来完成实例的创建与初始化,但开发者可以在此之前插入拦截逻辑,典型的应用便是单例模式(Singleton):在 `__call__` 中检查该类是否已经存在实例化对象,如果存在就直接返回旧实例,若不存在再调用底层机制创建新实例。

在高阶语法使用上,高天特别提出了一个“大坑”作为避坑提醒:在元类的方法中,如果使用 `super()` 代替直接显式调用 `type` 上的方法(如 `type.__call__`),必须注意传参形式的差异。在元类的 `__init__` 和 `__call__` 中,调用 `super()` 时需要去掉开头的 `self`(即 `cls`)参数;但是在 `__new__` 的 `super()` 调用中,该参数是不能去掉的。关于该行为底层的 CPython 机制与难啃的原代码,高天表示未来有机会再深入填坑讲解。

“这个 call 函数并不是在定义 class A 的时候被调用的,而是在你做 A 括号的时候被调用的,也就是一般来说这个 call 函数应该是你新建的这个类在尝试产生实例的时候被使用的。”

- 元类生命周期划分: - `__new__`:定义类时调用,负责创建并返回代表该类的对象。 - `__init__`:定义类时紧接着 `__new__` 调用,用于对已生成的类对象进行初始化操作。 - `__call__`:类在被实例化(如 `A()`)时调用,控制实例的生成过程。 - 单例模式可在元类的 `__call__` 中低侵入性实现,免去污染具体业务类内部代码的麻烦。 - 元类方法使用 `super()` 调用的参数差异:`__init__` 与 `__call__` 需移除显式的 `self`/`cls` 参数,而 `__new__` 则必须保留。

## Notable quotes > [00:00](https://www.youtube.com/watch?v=E_-Nl3My3mo&t=0s) 在 class A 的 definition 里面加了一个 metaclass 等于 M 的意思就是,你不要再用 type 来给我建立 class 了,你用这个 M 来给我建立 class。 — 码农高天

## Mentioned Python, CPython, 码农高天

## Video description 大家期待已久的话题metaclass终于来啦!由于这个内容本身就偏晦涩,我们这期视频主要从一个使用者的角度先去理解和认识metaclass,未来如果有机会,我们再去介绍它背后的机制!

[Source](https://www.youtube.com/watch?v=E_-Nl3My3mo)

中文简报

高天拆解Python元类机制 标定定义期构建与实例化拦截规则 元类在类定义阶段通过__new__与__init__完成属性约束与类对象构建,并在调用类生成实例时由__call__统一接管创建逻辑。 视频从 Python 中动态生成类的底层逻辑出发,系统阐述了元类的本质是用来替代默认的 type 来构建类对象的机制。高天深入梳理了元类的核心生命周期方法:__new__ 与 __init__ 在类定义声明阶段即被隐式触发,可用于在类构建期进行属性拦截与约束检查;而元类的 __call__ 则在类被实例化时触发,常用于实现单例模式等控制实例生成的逻辑。此外,视频还特别指出了在元类中使用 super() 调用时 __new__ 与 __init__、__call__ 传参规范不一致的常见开发陷阱。 元类(Metaclass)是类的“制造者”,声明 `class A(metaclass=M)` 并非继承,而是指示 Python 使用 `M` 代替默认的 `type` 来构建该类对象。 (0:00) 元类的 `__new__` 与 `__init__` 在类定义阶段立即执行,可用于对类属性进行强行校验、拦截或动态修改,解决普通继承无法覆盖的元编程场景。 (0:00) 元类的 `__call__` 函数在类实例化(如 `A()`)时触发,能直接控制对象的产生过程,是实现无侵入式单例模式的核心机制。 (5:00) 在元类中通过 `super()` 调用父类方法存在参数陷阱:`__init__` 与 `__call__` 需剔除显式的 `self`/`cls` 参数,而 `__new__` 则必须保留。 (5:00)

Reading summary

码农高天在讲解中指出,Python 中的元类(Metaclass)并非普通继承关系,而是取代默认的 `type` 来控制类的创建与实例化过程,赋予开发者在类定义和对象生成阶段插入元编程逻辑的能力。 元类的本质与类定义阶段的拦截机制 (0:00) 在 Python 中,静态声明 `class A:` 底层等价于调用 `type(name, bases, attrs)`。当显式声明 `class A(metaclass=M):` 时,并非让类 `A` 继承 `M`,而是指定使用元类 `M` 替代默认的 `type` 来构建该类对象。此时,类 `A` 实际上是元类 `M` 的一个实例对象。这一机制主要用于解决普通类继承无法处理的问题,赋予开发者在现有继承体系外动态增删或修改属性与方法的自由度。 类定义时的生命周期:__new__ 与 __init__ (5:00) 元类的 `__new__` 与 `__init__` 函数均在类被定义(而非类实例化)的瞬间隐式执行。高天通过在继承自 `type` 的元类 `M` 的 `__new__` 中打印类名、父类与属性字典证实了这一点。元类的 `__new__` 负责接收参数并创建类对象,可用于强行校验类定义,例如遍历属性字典若发现以 `test_` 开头的函数名则直接报错拦截。紧随其后执行的 `__init__` 则在类对象构建完成后运行,可直接对新生成的类对象进行初始化加工,例如向其挂载动态生成的随机属性。 控制实例化过程的 __call__ 与避坑要点 (5:00) 元类中的 `__call__` 函数不在类定义阶段运行,而是在执行 `o = A()` 创建类实例时被触发,其作用是接管并控制该类生成实例的过程。该机制的典型应用是实现单例模式:在元类的 `__call__` 中检查是否已有实例存在,若有则直接复用,避免对具体业务类代码造成污染。此外,高天提醒在使用 `super()` 调用底层机制时存在语法陷阱:在元类的 `__init__` 与 `__call__` 中调用 `super()` 时需要去掉开头的 `self`(即 `cls`)参数,而在 `__new__` 中调用 `super()` 时则必须保留该参数。 元类通过在类定义期运行 `__new__` 与 `__init__`,以及在实例化时运行 `__call__`,提供了底层的元编程控制力;高天同时提及,关于使用 `super()` 时参数差异背后的 CPython 源码底层实现机制,仍有待未来进一步展开讲解。

Source quotes

[0:00] 码农高天: 在 class A 的 definition 里面加了一个 metaclass 等于 M 的意思就是,你不要再用 type 来给我建立 class 了,你用这个 M 来给我建立 class。

Original transcript

[0:00] 这期视频也算是千呼万唤使出来这可能是我看到的大家最想听的话题之一了那我们今天就来讲一下原类MetaclassMetaclass这个概念本身就比较晦涩它涉及的东西就比较多所以我们这期视频做一个入门视频我们主要从概念和应用的角度来聊这个Metaclass就不去深究它背后的实现原理了MetaClass是什么呢先给大家一个基本的概念我们看左边的程序有一个Class M它继承了这个type然后它里面有一个new函数然后返回type的new接下来我们定义了一个Class A这个A的括号里面写了一个MetaClass等于M注意如果我们平时是A继承M的话那就是A括号M但是这块是A meta class等于M这个时候我们就说M是A的原类当然讲到这儿是跟没说一样的对吧它到底是什么意思呢我们在之前的一期视频里面讲过我们可以静态的定义一个class也可以动态的生成一个class现在我们的这第五行第六行的极简定义跟我们第八行的这个动态的生成是完全等价的那实际上我们在用class A来静态的定义一个class的时候它背后本质上也是相当于调用了这个type class如果这个部分你还没理解把我之前的那期视频先看完好 那我们现在已知通过type可以建立一个class我们只需要传进去三个数据它的名字 它继承的负类和它所有的attributes那么我会很自然的去想有没有可能让这个事情变得更加灵活让我们可以自定义一些中间的过程因为type就直接调用到c函数里面了我们想做这么一件事我们想做一个a等于m括号stringa然后负类然后attributes然后让m返回我需要的类而这个就是meta class的初始逻辑在这里可以直接告诉大家刚才我们说的第9行修改过的代码它就等价于在原来我们classA的定义里面加了一个meta class等于m在classA的definition里面加了meta class等于m的意思就是你不要再用type来给我建立class了你用这个m来给我建立class当然在我们的实际应用中绝大多数情况下这个原类m依然是要用type来建立class的这里我再来说一下逻辑Python里面所有东西都是object 对吧那现在a是m这个class的一个object当我在使用o等于a括号的时候o又是a这个class的一个object它们之间的关系你要想明白那我们知道当我们写o等于a括号的时候我们实际上会调用classa的new函数和init函数同样的道理 我们在做a等于m括号的时候 也会调用m的new函数和init函数只不过在我们平时写成这种classa括号metaclass等于m的形式的时候这个调用不是显示的 是隐式的但是就像我们前面说的他也在里面调用了一个m括号三个参数所以我们就可以通过重载M的new函数在建立这个class过程中动一些手脚我们来看左边这边代码的结果我在classM的new函数里面print了name,basis和dict这里大家一定要注意我只声明了一个classA我并没有用A产生任何的实例但是我们能看到右边我们实际上是打印出来了这个name basis和dict了这也就是说明m的这个new函数是在我定义这个classa的时候被执行的那可能有人要问这个东西有什么用啊meta class一般是用来解决继承没有办法解决的问题的我们随便举个例子比如说我想规定某一些类里面不可以出现比如说test下滑线开头的函数我就可以写这么一段程序我把所有的dict里面的keys都找一遍看它是不是test下滑线开头如果是的话比如我就raise一个error这样我在尝试定义一个有test下滑线开头的class的时候就会爆错meta class本身就是让你可以更灵活的掌握建立类型的 [5:00] 中间的这个逻辑比如我们可以通过class里面定义的成员来增加或者删减我这个类里面的方法总之就是在现有的这个继承体系里 你感觉到无法实现的事情你可以考虑用原类来完成那和python其他所有的object一样,这个meta class m也可以有init凳数,这个init凳数同样也是在class a被定义的时候调用的它是在new函数之后被调用的那init跟new最大的区别呢就是在你运行init函数的时候已经有a这个object了注意这个A是一个type是一个m的实例那在M里呢就可以对新建的这个class做一些手脚我们举个非常简单的例子 在init函数里面 我把self.randomid给随机复制了一个随机值那我在建立这个class A之后 我可以打印它的random ID就可以看到是一个0到100之间的随机值当然我举这个例子非常简单啊,它里面没有什么复杂的逻辑所以他是等价于你在定义class A的时候直接把里面random ID给复制成一个随机制的就像我前面说到的 当你需要用到相对复杂的逻辑的时候你可能才会需要用到meta class接下来我们说一下meta class里面实际上你还可以定义下围线下围线靠函数这个靠函数一定要注意一下它跟new和init不太一样这里非常容易搞混这个靠函数并不是在定义class A的时候被调用的而是在你做A括号的时候被调用的也就是一般来说这个靠函数应该是你新建的这个类在尝试产生实力的时候被使用的我们看左边的代码我们产生了两个a的实例于是就调用了两次这个靠函数一般来说我们就直接使用type的call函数来完成它的主要任务也就是产生一个实力但是在之前我们可以增加其他的逻辑比如我们前面讲的Singleton就可以在这里判断是不是已经有了一个object了如果有直接返回那个 如果没有再新建一个最后大家要注意一下如果你在原类的method里使用 super 而不是直接 type.r来调用这个负类的函数的话在init和call里面你是要把这个self class给去掉的但是在new这个调用里面是不可以去掉的那这是为什么呢这块有个大坑啊 我们未来再慢慢舔好 那今天视频就到这里我们今天讲的是比较入门和基础的meta class相关的内容如果大家感兴趣的话呢我们在未来会再深入的介绍一下它背后的机制因为meta class这个部分背后的原代码相对来说是比较难读的好那喜欢的话记得点一下关注别忘了一键三连哦字幕志愿者 杨茜茜