码农高天
类属性查找通过底层type_getattro无条件触发描述器协议,使staticmethod实时解包为裸函数,并确保classmethod在任何调用方式下均自动绑定类对象作为首参数。
AI-assisted summary of 【python】staticmethod与classmethod深度机制解析——要知其所以然 — original source. Check the original for full context.
Source published:
Key ideas
- staticmethod 和 classmethod 均为内置装饰器,但传参机制截然不同:静态方法不隐式注入任何首参数,而类方法无论通过类还是实例调用,首个实参始终自动绑定为当前类对象。 (0:00)
- 类字典 A.__dict__['f'] 中实际存储的是 staticmethod 或 classmethod 实例,直接通过点号访问时能拿到函数或绑定方法,完全依赖于底层描述器协议(Descriptor Protocol)。 (5:00)
- 类属性查找遵循专门的 type_getattro 机制,只要查找到的对象定义了 tp_descr_get,无论其是否为数据描述器,都会无条件调用其 __get__ 方法进行解析。 (5:00)
- classmethod 的描述器在被触发时,会提取当前类或实例所属的 type,并将其与原函数包装成首参数固定的 bound method 返回。 (10:00)
Detailed notes
## Background 该视频由“码农高天”于2024年2月9日发布在YouTube平台上。视频简介提到,本期内容深入探讨Python中的`staticmethod`和`classmethod`及其背后的实现机制,挖掘开发者平时容易忽略的底层细节。
## TL;DR 视频首先对比了`staticmethod`与`classmethod`在表层用法上的异同,强调静态方法本质上是置于类命名空间下的普通函数,而类方法则无论通过类还是实例调用都会自动隐式传入当前类型对象。随后,作者深入探究了底层实现,指出类属性字典中实际保存的是内置的装饰器包装对象。通过结合CPython源码与描述器协议(Descriptor Protocol),视频详细剖析了`type_getattro`查找属性时触发`tp_descr_get`的解包与绑定机制,揭示了`staticmethod`返回原生函数而`classmethod`返回绑定类对象的bound method的真正原因。
## Atmosphere & style 视频整体呈现出硬核、严谨且循序渐进的技术极客风格。主讲人码农高天语气平实自然,直奔底层原理,毫不拖泥带水,展现出一种“不仅知其然,更要知其所以然”的探究精神。观众跟随着从最基础的Python语法逐渐下潜到C源码结构体与字节码调度,能清晰感受到层层剥茧后的豁然开朗。
## Bulletin - [00:00](https://www.youtube.com/watch?v=j878XzC6J54&t=0s) staticmethod 和 classmethod 均为内置装饰器,但传参机制截然不同:静态方法不隐式注入任何首参数,而类方法无论通过类还是实例调用,首个实参始终自动绑定为当前类对象。 - [05:00](https://www.youtube.com/watch?v=j878XzC6J54&t=300s) 类字典 A.__dict__['f'] 中实际存储的是 staticmethod 或 classmethod 实例,直接通过点号访问时能拿到函数或绑定方法,完全依赖于底层描述器协议(Descriptor Protocol)。 - [05:00](https://www.youtube.com/watch?v=j878XzC6J54&t=300s) 类属性查找遵循专门的 type_getattro 机制,只要查找到的对象定义了 tp_descr_get,无论其是否为数据描述器,都会无条件调用其 __get__ 方法进行解析。 - [10:00](https://www.youtube.com/watch?v=j878XzC6J54&t=600s) classmethod 的描述器在被触发时,会提取当前类或实例所属的 type,并将其与原函数包装成首参数固定的 bound method 返回。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=j878XzC6J54&t=0s) staticmethod 将普通函数归纳在类命名空间中,既不隐式接收 self 也不隐式接收 cls。 - [00:00](https://www.youtube.com/watch?v=j878XzC6J54&t=0s) 通过类名 A.f(...) 或实例对象 a.f(...) 均可调用 staticmethod,两者的执行效果完全一致。 - [00:00](https://www.youtube.com/watch?v=j878XzC6J54&t=0s) classmethod 会自动将类本身作为第一个实参传入,调用者无需显式传递首个参数。 - [00:00](https://www.youtube.com/watch?v=j878XzC6J54&t=0s) 通过实例对象调用 classmethod 时,隐式传入的首个实参依然是该对象的所属类(type),绝不会变成实例对象本身。 - [05:00](https://www.youtube.com/watch?v=j878XzC6J54&t=300s) 直接通过 A.f 访问属性返回的是函数对象,而查询类底层属性字典 A.__dict__['f'] 得到的则是 staticmethod 实例对象。 - [05:00](https://www.youtube.com/watch?v=j878XzC6J54&t=300s) staticmethod 在 CPython 内部对应 PyStaticMethod_Type,属于非数据描述器(Non-data Descriptor)。 - [05:00](https://www.youtube.com/watch?v=j878XzC6J54&t=300s) staticmethod 的描述器实现函数 tp_descr_get 逻辑简单,其核心就是直接解包并返回内部持有的原始 callable 函数。 - [05:00](https://www.youtube.com/watch?v=j878XzC6J54&t=300s) 对类进行属性访问走的是针对 Type 的专属查找路径 type_getattro,而非普通对象的属性查找逻辑。 - [05:00](https://www.youtube.com/watch?v=j878XzC6J54&t=300s) 在 type_getattro 流程中,只要属性对象实现了 tp_descr_get,都会被无条件调用并触发描述器求值。 - [05:00](https://www.youtube.com/watch?v=j878XzC6J54&t=300s) 未被装饰的普通函数自身也是描述器,当通过类访问且 obj 参数为 None 时,直接将原始函数自身原样返回。 - [05:00](https://www.youtube.com/watch?v=j878XzC6J54&t=300s) 普通函数只有在 obj 不为空(通过实例访问)时,其描述器才会返回与该实例绑定的 method 对象。 - [10:00](https://www.youtube.com/watch?v=j878XzC6J54&t=600s) classmethod 的描述器与 staticmethod 不同,它不会返回原始裸函数,而是返回一个绑定了类对象的 bound method。 - [10:00](https://www.youtube.com/watch?v=j878XzC6J54&t=600s) 通过类访问 classmethod 时,type_getattro 会把当前访问的类对象作为第三个参数直接传给描述器的 descr_get。 - [10:00](https://www.youtube.com/watch?v=j878XzC6J54&t=600s) 通过实例访问 classmethod 时,通用属性查找逻辑会获取该实例对应的所属类型 type(a) 并传入描述器完成绑定。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=j878XzC6J54&t=0s) staticmethod 与 classmethod 的基本用法与区别
在 Python 中,`staticmethod` 和 `classmethod` 是两个非常常用的内置装饰器(built-in decorator),它们最显著的共同特点是通常被用于类定义(class definition)内部。在普通情况下,我们在类中定义方法时,第一个形参通常约定为 `self`,这是一个在调用方法时自动将调用该方法的对象实例传递进去的机制。然而,`staticmethod` 截然不同。被 `@staticmethod` 装饰的函数实际上就是一个极其普通的函数,其调用方式与在类外部定义的全局函数完全一样,这个装饰器本质上只是将该函数置于类的命名空间下,使其在逻辑组织上从属于这个类。使用 `staticmethod` 的核心优势体现在两方面:第一,它提供了更好的代码封装性,将关联逻辑整合进类命名空间;第二,调用它无需实例化类的对象,可以直接通过 `A.f(...)` 类名加函数名的方式调用。当然,如果手头恰好有一个该类的实例对象,通过 `A().f(...)` 实例来调用也是完全支持的,两者的执行效果毫无差异。
相比之下,`classmethod` 虽然同样是将函数绑定在类本身而非类的实例对象上,但两者的参数传递机制存在明显区别。码农高天特别强调了参数的特性:“我们回忆一下 static method 是没有传进来 class 的,它只有你函数调用时候的 argument,但是 class method 会把这个 class 当作第一个参数传进来。”在实际编写业务逻辑时,被 `@classmethod` 装饰的函数第一个形参约定为 `cls`,后面再接调用参数;而在真正调用时,调用者不需要显式传入类参数,Python 会自动将该类对象作为第一个参数压入。这与普通方法将实例 `self` 作为首个参数自动传入的体验高度相似。
此外,`classmethod` 无论通过类直接调用还是通过实例对象调用,其传参规则保持绝对一致。即便使用 `A()` 实例化对象来调用该方法,底层自动传入的第一个参数依然是该对象所属的类 `A`(即其 `type`),而绝对不会变成实例对象本身。码农高天指出:“一定注意,无论是在 class 上面调用,还是在 object 上面调用,它进来的这第一个 argument,class 都是 class 啊,就即使你是 A 括号建立了一个 object 调用,它的第一个 argument 这个 class 依然是它的 type 也就是 a,这里就是这个 class method 跟我们一般的 method 不同的地方,我们一般的 method 第一个 self 是 object。”
- **staticmethod 的特点**:不隐式传递任何额外参数(既不传 `self` 也不传 `cls`);可以通过 `A.f()` 或 `a.f()` 调用;主要起到将普通功能函数归纳在类命名空间内的封装作用。 - **classmethod 的特点**:隐式传递当前类(`cls`)作为第一个实参;无论使用 `A.f()` 还是 `a.f()` 调用,首参数均为当前类型对象;常用于工厂方法或需要感知类属性的场景。
```mermaid flowchart TD accTitle: 普通方法、staticmethod 与 classmethod 的首参数传递机制对比 accDescr: 展示三种方法在通过类或对象调用时,首个隐式参数传递的差异。
subgraph RegularMethod ["普通方法 (Regular Method)"] R_Obj["obj.method(...)"] -->|"隐式传入"| R_Self["self (实例对象 obj)"] R_Cls["Cls.method(...)"] -->|"需显式传参或报错"| R_Err["无自动绑定"] end
subgraph StaticMethod ["静态方法 (@staticmethod)"] S_Call["Cls.f(...) 或 obj.f(...)"] -->|"无隐式传参"| S_Pure["普通函数执行 (仅接收显式参数)"] end
subgraph ClassMethod ["类方法 (@classmethod)"] C_Cls["Cls.f(...)"] -->|"隐式传入"| C_Class["cls (当前类 Cls)"] C_Obj["obj.f(...)"] -->|"隐式传入实例的 type"| C_Class end ```
---
[05:00](https://www.youtube.com/watch?v=j878XzC6J54&t=300s) 底层机制剖析:为什么打印 a.f 得到的是函数?
在明确两者的表层用法之后,深入 Python 底层的核心机制才是关键。码农高天直言:“那你可能觉得这些内容很简单对吧,或者你之前也听过,那如果你之前看过我的视频就肯定会知道,我视频的重点从来就不在怎么使用它们,而是它们是怎么实现的呢?”
如果直接执行 `print(A.f)`,终端会显示它是一个函数(function)。然而,类定义体内部的代码执行完毕后,所有属性都会被存入类属性字典 `A.__dict__` 中。如果进一步探查底层,打印 `A.__dict__['f']`,就会发现它存入的实际是一个 `staticmethod` 对象(`staticmethod object`)。从 CPython 源码来看,`staticmethod` 本身就是一个内置类型 `PyStaticMethod_Type`。既然保存在字典里的是一个静态方法包装对象,为什么通过 `A.f` 读取时拿到的却是解包后的原始函数?其核心机制在于 Python 的描述器协议(Descriptor Protocol)。
`PyStaticMethod_Type` 结构体中实现了描述器获取函数 `tp_descr_get`。在 staticmethod 的 descriptor get 逻辑中,它仅仅是简单地返回其内部存储的 callable(即初始化时传入的原函数 `sm_callable`)。但是这里引申出了一个极具迷惑性的边界问题:在 Python 的属性查找规则中,一个对象查找属性时通常优先查看是否存在数据描述器(Data Descriptor,即同时实现了 `__get__` 和 `__set__`);而 `staticmethod` 并没有定义 `__set__`,属于非数据描述器(Non-data Descriptor)。按照常规对象的查找机制,Python 应当优先去实例自身的 `__dict__` 中找,那为什么通过类访问属性时依然会触发非数据描述器的 `__get__` 呢?
原因在于对类(Type)进行属性访问时,走的是专门针对类型的查找逻辑——`type_getattro`(对应 C 层的 `PyType_Type.tp_getattro`),而不是普通对象的 `object_generic_getattr`。在执行字节码 `LOAD_ATTR` 查找类的属性时,`type_getattro` 会通过 `_PyType_Lookup` 在类的继承链上查找属性,拿到该属性对象后,若发现它定义了 `tp_descr_get`,无论它是不是数据描述器,都会无条件调用它的描述器 `__get__` 方法。
对于普通类函数而言,当用户直接通过类去获取方法(如访问未被装饰的普通函数 `A.func`)时,拿到的也是一个原始 `function` 而非绑定方法(bound method)。这是因为函数的 `descr_get` 函数在被调用时,接收的第二个参数目标对象 `obj` 为 `NULL`(即在 Python 层表现为 `None`)。当 `obj` 为 `None` 时,函数的描述器实现直接将其自身(即原函数)原样返回;只有当 `obj` 不为空(通过实例访问方法)时,它才会返回一个将对象和函数绑定在一起的 `method` 对象。
- **`A.f` 与 `A.__dict__['f']` 的差异**:`A.__dict__['f']` 绕过描述器机制,直接获取底层字典存储的 `staticmethod` 实例;而 `A.f` 会触发 `tp_descr_get`,返回内部包装的原函数对象。 - **`type_getattro` 查找特性**:类属性访问遵循针对 Type 的专属属性查找逻辑,只要属性存在描述器 `__get__`,就会直接对其求值并返回其结果。
---
[10:00](https://www.youtube.com/watch?v=j878XzC6J54&t=600s) classmethod 的描述器绑定逻辑与总结
理解了 `staticmethod` 的描述器解包过程后,`classmethod` 的实现机制也就迎刃而解。`classmethod` 与 `staticmethod` 的核心实现路径非常相似,但其描述器的 `tp_descr_get` 函数做的事情更为关键:它返回的不再是裸函数,而是通过底层逻辑将原函数与类型对象(type)绑定在一起的绑定方法(bound method)。
在 `type_getattro` 的源码中,当它调用目标描述器的 `descr_get` 时,会将当前访问的类作为第三个参数(`type` 参数)传递进去。由此建立了一条严密的绑定链路: 1. **通过类直接调用(`A.f`)**:`type_getattro` 直接将类 `A` 作为第三个参数传进 `classmethod` 的描述器函数,描述器直接将该函数和该类绑定,返回一个首参数固定为类 `A` 的 bound method。 2. **通过实例对象调用(`a.f`)**:在普通对象的 `object_generic_getattr` 流程中,会检测到类属性字典中包含定义了 `tp_descr_get` 的 `classmethod` 对象,此时获取该实例的所属类 `type(a)` 并一同传入描述器中,描述器同样将函数与该所属类完成绑定。
正是这一段统一的描述器逻辑,使得开发者在调用端无论是直接使用类本身调用,还是使用该类的任何实例对象调用,`classmethod` 底层都可以稳定提取出目标类型本身,从而保证了运行时传给函数的第一个实参永远是类对象本身。
通过对 `staticmethod` 和 `classmethod` 底层机制的拆解,展示了从类字典存储、`LOAD_ATTR` 字节码调度到描述器协议(Descriptor Protocol)协作的全流程,揭示了表面看似简单的语法糖背后严谨的 CPython 底层实现。
## Notable quotes > [00:00](https://www.youtube.com/watch?v=j878XzC6J54&t=0s) static method 是没有传进来 class 的,它只有你函数调用时候的 argument,但是 class method 会把这个 class 当作第一个参数传进来。 — 码农高天
## Mentioned Python, CPython, staticmethod, classmethod, 码农高天
## Video description 这期视频聊一下staticmethod和classmethod,以及它们背后的实现机制。肯定有你没认真想过的角落~
[Source](https://www.youtube.com/watch?v=j878XzC6J54)