Python C扩展传参机制解析 FASTCALL直传指针打破性能瓶颈

【python】C扩展里这些METH_都是干啥的?聊聊C模块的方法传参 — 码农高天

码农高天

开发者普遍依赖通用元组打包处理C扩展参数,但在高频调用场景下需采用METH_FASTCALL直接传递原生指针数组以消除元组创建与解包开销。

AI-assisted summary of 【python】C扩展里这些METH_都是干啥的?聊聊C模块的方法传参 — original source. Check the original for full context.

Source published:

Key ideas

  • Python C 扩展通过 PyMethodDef 结构体数组注册类方法,末尾必须包含全为 NULL/0 的哨兵元素以弥补 C 语言数组传参丢失长度信息的缺陷。 (0:00)
  • 通用传参标志 METH_VARARGS 配合 PyArg_ParseTuple 可以解析位置参数,但在高频回调场景下会产生创建元组和动态解包的性能开销。 (3:40)
  • 针对不同传参场景,Python C API 提供了 METH_NOARGS、METH_O 以及按位或组合的 METH_VARARGS | METH_KEYWORDS,能分别优化零参、单参及位置加关键字传参的处理逻辑。 (6:40)
  • cProfile 等高频调用的性能敏感模块采用 METH_FASTCALL 机制,直接传递 C 原生指针数组和长度,彻底消除临时元组打包的性能开销。 (10:00)

Detailed notes

## Background 该视频由“码农高天”于2024年11月16日发布在YouTube平台。视频简介指出,许多人在编写Python C扩展时并不清楚定义方法时传入的参数标志具体含义、它们对应了Python的哪种调用形式,以及如何在C语言端正确获取Python参数,本期内容旨在深入剖析这些底层实现细节。

## TL;DR 视频系统性地解析了Python C扩展中定义类方法的核心结构体`PyMethodDef`及其字段含义,重点阐释了末尾哨兵的作用与`.pyi`存根文件的生成机制。随后,作者对比了位置参数模式`METH_VARARGS`配合`PyArg_ParseTuple`的解析方案,以及`METH_NOARGS`、`METH_O`和`METH_VARARGS | METH_KEYWORDS`等贴合不同入参场景的高效选项。最后,针对Profiler等高频调用的极端性能需求,视频介绍了`METH_FASTCALL`如何通过消除元组打包开销来实现极致性能优化。

## Atmosphere & style 视频呈现出严谨扎实的技术硬核风格,讲解节奏明快且条理极其清晰。博主结合自己开源项目VizTracer和cProfile的实战经验现身说法,将看似晦涩枯燥的C API文档规范拆解得通俗易懂,给人一种资深工程师在白板前耐心传授底层心法的沉浸感与通透感。

## Bulletin - [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) Python C 扩展通过 PyMethodDef 结构体数组注册类方法,末尾必须包含全为 NULL/0 的哨兵元素以弥补 C 语言数组传参丢失长度信息的缺陷。 - [03:40](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=220s) 通用传参标志 METH_VARARGS 配合 PyArg_ParseTuple 可以解析位置参数,但在高频回调场景下会产生创建元组和动态解包的性能开销。 - [06:40](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=400s) 针对不同传参场景,Python C API 提供了 METH_NOARGS、METH_O 以及按位或组合的 METH_VARARGS | METH_KEYWORDS,能分别优化零参、单参及位置加关键字传参的处理逻辑。 - [10:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=600s) cProfile 等高频调用的性能敏感模块采用 METH_FASTCALL 机制,直接传递 C 原生指针数组和长度,彻底消除临时元组打包的性能开销。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) Python 类在 C 扩展中定义时,自定义方法统一挂载在 PyTypeObject 的 tp_methods 字段中。 - [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) PyMethodDef 结构体包含方法名(ml_name)、函数指针(ml_meth)、传参标志(ml_flags)和文档字符串(ml_doc)四个核心字段。 - [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) 由于 C 语言数组在传参时退化为指针且丢失长度,PyMethodDef 数组必须使用哨兵元素 {NULL, NULL, 0, NULL} 标示结尾。 - [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) IDE 中点击跳转到只有类型签名与省略号的 .pyi 存根文件,表明底层由 C 扩展实现,存根仅用于辅助类型检查和代码补全。 - [03:40](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=220s) Python 函数签名末尾的斜杠表示仅限位置参数,调用端无法使用关键字形式传入参数。 - [03:40](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=220s) METH_VARARGS 模式下底层 C 函数接收 self 和 args 元组,通常使用 PyArg_ParseTuple 及格式化字符串(如 "OsO")解析参数。 - [06:40](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=400s) METH_NOARGS 声明无参数方法,调用时的参数校验由 Python 运行时自动处理,C 函数可通过 Py_UNUSED 宏消除未使用形参告警。 - [06:40](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=400s) METH_O 专用于严格单一参数的方法,直接将参数作为 PyObject 指针传递,省去了打包与解包元组的开销。 - [06:40](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=400s) 由于标志位本质是按位左移的数值,支持关键字参数时使用按位或组合 METH_VARARGS | METH_KEYWORDS,并扩展为包含 kwargs 的三参数签名。 - [06:40](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=400s) 解析关键字参数需使用 PyArg_ParseTupleAndKeywords,并传入以 NULL 结尾的静态字符串数组作为关键字名称列表。 - [06:40](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=400s) 在 PyArg_ParseTuple 格式化字符串中使用竖线(如 "|l")可指定后续参数为可选参数,未传参时保留 C 变量初始值。 - [10:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=600s) METH_FASTCALL 机制专为极致性能设计,直接传递 PyObject *const *args 和 nargs,彻底消除元组创建及垃圾回收开销。 - [10:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=600s) 使用 METH_FASTCALL 时无法使用 PyArg_ParseTuple 等封装工具,开发者需要直接按索引从指针数组中提取参数。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) C 扩展中定义类方法的核心骨架与 `PyMethodDef`

许多开发者在初次尝试编写 Python C 扩展(C Extension)时,通常都是“照猫画虎”——要么从官方文档里抄一段模板,要么参考开源项目的现有实现;如今有了 ChatGPT,搭出一个能跑的 C 扩展框架更是轻而易举。然而,大家往往忽略了其中面向 Python 暴露方法时的接口(Interface)细节:在 C 语言中如何声明这个方法、传参标志到底代表什么,以及在 C 代码内部究竟该如何正确解析由 Python 传入的参数。

在典型的 C 扩展开发中,通常会为 Python 定义一个新类(即 `PyTypeObject`),虽然模块(Module)本身也可以包含独立函数,但核心逻辑大多落在类的自定义方法上。诸如 `tp_new` 和 `tp_init` 这类内置魔术方法有专门的固定位置,而开发者自定义的类方法则统一存放在 `tp_methods` 字段中。该字段本质上是一个由 `PyMethodDef` 结构体组成的数组,用于注册该类对 Python 暴露的所有接口。每个 `PyMethodDef` 元素均包含四个部分:

```mermaid flowchart LR accTitle: PyMethodDef 结构体构成 accDescr: 展示 PyMethodDef 四个字段及结尾哨兵的对应关系 subgraph Def["PyMethodDef 结构体元素"] F1["ml_name<br/>(Python 中调用的方法名)"] F2["ml_meth<br/>(C 实现函数指针,转为 PyCFunction)"] F3["ml_flags<br/>(传参标志:METH_VARARGS 等)"] F4["ml_doc<br/>(方法的文档字符串 docstring)"] end Def --> Sentinel["{NULL, NULL, 0, NULL}<br/>(哨兵 Sentinel:标识数组结尾)"] ```

在编写这个数组时,末尾必须补上一个全空的元素作为哨兵(Sentinel)。码农高天强调:“因为我们知道 C 的 array 在进行传参的时候它会丢失掉长度这个信息对吧,那这个 Sentinel 就是常见的告诉你最后一个已经到了的方式。”此外,在日常开发中使用 VS Code 等 IDE 按住 `Ctrl` 点击方法时,经常会跳转到只有类型签名、实现全为省略号(`...`)的 `.pyi` 存根文件,很多人不清楚其原理。这实际上表明底层实现在 C 扩展中完成,源码无法直接阅读,但 `.pyi` 文件把接口和签名暴露出来,专门用于辅助类型注解(Type Hints)与 IDE 补全。

- **`ml_name`**:对应 Python 脚本中调用的方法名称。 - **`ml_meth`**:C 函数的指针,强制转换为 `(PyCFunction)`,此指针级别的类型转换主要用于避免编译器告警。 - **`ml_flags`**:描述该方法在 Python 调用时的传参规范(如 `METH_VARARGS` 等),决定了 C 层的参数接收形式。 - **`ml_doc`**:方法的文档字符串(Documentation)。 - **哨兵元素(Sentinel)**:由于 C 语言数组传参退化为指针、不保留长度信息,数组最后必须以 `{NULL, NULL, 0, NULL}` 结尾,用于明确告知 CPython 运行时方法定义列表到此结束。

---

[03:40](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=220s) 位置参数模式:`METH_VARARGS` 与 `PyArg_ParseTuple`

以一个用于传递给 `sys.setprofile` 的内部跟踪回调函数 `thread_trace_func` 为例,其在 `.pyi` 里的签名通常为 3 个仅限位置参数(Positional-only Arguments),签名末尾带有斜杠(`/`)。斜杠表示其前面的所有参数都绝不能以关键字参数(Keyword Arguments)的形式传入,调用方必须按顺序传值,不能写 `frame=...` 或 `event=...`。

针对这种全部以位置形式传参的 Python 函数,对应的标志位为 `METH_VARARGS`。此时底层 C 函数固定接收两个参数:第一个是 `PyObject *self`(代表调用该方法的实例本身),第二个是 `PyObject *args`(一个装有所有位置参数的 Python 元组 `tuple`)。在 C 语言中解析此类参数,最通用的手段是调用 `PyArg_ParseTuple`。通过格式化字符串(如 `"OsO"`),运行时会按顺序将 tuple 里的项依次提取并赋值到 C 语言的对应变量中:

```c // 示例:从 args 元组中提取 frame (PyObject*), event (char*), trace_args (PyObject*) PyObject *frame; char *event; PyObject *trace_args; if (!PyArg_ParseTuple(args, "OsO", &frame, &event, &trace_args)) { return NULL; } ```

格式字符串中的 `O` 代表原封不动地提取为 Python 原生对象指针(`PyObject*`),而 `s` 则会自动将 Python 的 `str` 对象转换为以 null 结尾的 C 字符串指针(`char *`)。码农高天结合自己的开源项目经验分享道:“实话跟大家说 VizTracer 在很长一段时间内都是这么干的,就只有这一种 interface 的方式。”虽然开发者完全可以在 C 扩展外层包裹一层 Python 代码做参数校验,然后一律用位置参数强行传进 C 层,但这绝非在所有场景下都是最优雅、最高效的设计。

---

[06:40](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=400s) 无参数、单参数与关键字参数:`METH_NOARGS`、`METH_O` 及 `METH_KEYWORDS`

除了通用的元组打包解析,Python C API 还提供了更贴合特定传参场景的高效选项:

```mermaid flowchart TD accTitle: Python C 扩展方法传参标志与处理方式 accDescr: 根据参数特征选择对应的 METH 标志以及 C 函数参数特征 Start{"方法传参特征"} Start -->|"不需要任何参数"| NA["METH_NOARGS<br/>(检查参数为空的工作由 Python 运行时完成)"] Start -->|"正好只有一个参数"| MO["METH_O<br/>(直接传入单一 PyObject*,无需解包 tuple)"] Start -->|"仅支持位置参数"| VA["METH_VARARGS<br/>(args 为 tuple,使用 PyArg_ParseTuple 解析)"] Start -->|"支持位置 + 关键字参数"| VK["METH_VARARGS | METH_KEYWORDS<br/>(额外接收 kwargs 字典,使用 PyArg_ParseTupleAndKeywords)"] ```

1. **无参数模式(`METH_NOARGS`)**: 对于不接收任何参数的方法,直接声明 `METH_NOARGS` 即可。此时 Python 内部会自动进行校验,一旦调用端传入了参数就会自动拦截抛错。对应的 C 函数通常依然写成两个参数的形式,但由于第二个参数完全用不上,可以将其命名为 `unused` 并使用宏 `Py_UNUSED(unused)` 修饰,以杜绝编译器的“未引用变量”警告。 2. **单一参数模式(`METH_O`)**: 如果方法严格只接收一个参数,使用 `METH_O` 是最便捷的选择。CPython 会在调用该 C 函数前直接将唯一的参数作为 `PyObject *` 传入函数的第二个形参中,省去了创建元组与从元组解包的额外开销。 3. **位置与关键字混合模式(`METH_VARARGS | METH_KEYWORDS`)**: 当方法既支持位置参数又支持关键字传参(类似于 `*args, **kwargs`)时,标志位需要通过按位或(Bitwise OR)组合起来。码农高天解释说:“因为这些 flags 的本质都是 1 左移某个数,所以可以互相 or 起来。”此时对应的 C 函数签名会扩展为三个参数:`self`、`args`(元组)以及 `kwargs`(字典)。在提取参数时,必须使用专门的 `PyArg_ParseTupleAndKeywords` 函数,并额外提供一个以 `NULL` 结尾的静态字符串数组(`keyword_list`)充当哨兵,以定义各个位置参数对应的关键字名称: ```c static char *kwlist[] = {"name", "args", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO", kwlist, &name, &trace_args)) { return NULL; } ``` 4. **可选参数处理**: 若某些参数允许缺省,可以在解析格式字符串中加入竖线 `|`(例如 `"|l"`)。竖线表示从该标记开始往后的所有参数均为可选参数。在解析前给目标 C 变量赋初始值,若 Python 传入了该参数则会转为对应的 C 类型(例如 `l` 转换为 `long`)并覆盖该变量,未传入时则保留原值;如果没有竖线且未提供参数,`PyArg_ParseTuple` 将直接报错返回。

---

[10:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=600s) 极致性能调优:`METH_FASTCALL`

在一般的性能分析或追踪工具中,回调函数的触发频率可能极高,普通传参带来的元组创建和解包开销会变得无法忽视。码农高天指出:“比如说在 cProfile 这个 module 里,它的底层是一个 C 写的 profiler,大家知道 Profiler 的 callback 对性能是非常敏感的,他肯定是希望用最小的代价来完成这个调用。”

为了满足极致性能的要求,CPython 提供了 `METH_FASTCALL` 机制:

| 特性维度 | `METH_VARARGS` 模式 | `METH_FASTCALL` 模式 | |:--- |:--- |:--- | | **底层实现机制** | 将所有位置参数打包为一个临时的 Python `tuple` 对象后传入 | 不进行元组打包,直接传入 C 原生指针数组及长度 | | **C 函数参数签名** | `(PyObject *self, PyObject *args)` | `(PyObject *self, PyObject *const *args, Py_ssize_t nargs)` | | **关键字参数支持** | 配合 `METH_KEYWORDS`,接收 `kwargs` 字典 | 配合 `METH_KEYWORDS`,接收元组记录所有传入的关键字名 | | **性能表现与开销** | 存在元组创建、垃圾回收及 `ParseTuple` 解析的额外性能开销 | 零打包开销,调用速度极快,性能极高 | | **适用场景** | 业务逻辑复杂、参数形式多变、更注重代码易读性的通用场景 | 回调触发频率极高(如 Profiler 回调)、参数简单的性能瓶颈代码 |

由于 `Fastcall` 绕过了标准元组封装,开发者无法直接利用 `PyArg_ParseTuple` 这类高阶工具解析参数,必须手动通过索引从 `args` 数组中按需取值。此外,`METH_FASTCALL` 同样支持与 `METH_KEYWORDS` 进行按位或组合以支持关键字传参。码农高天总结道,对于同时熟练掌握 Python 和 C 语言的开发者来说,编写 C 扩展最容易让人困惑的门槛往往正是这个接口层规范,弄懂这些 `METH_` 标志的含义与机制能够彻底理清 Python 调用 C 函数的传参底层逻辑。

## Mentioned 码农高天, Python, CPython, C 语言, ChatGPT, VS Code, VizTracer, cProfile, PyMethodDef, PyArg_ParseTuple, PyArg_ParseTupleAndKeywords

## Video description 可能有很多人并不知道Python的C extension里,当我们定义一个面向Python的方法的时候,给的这个参数是什么意思,分别对应了哪种Python的调用形式,以及在C语言中如何获取Python参数。这期视频我们来好好聊聊这个细节。

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

中文简报

Python C扩展传参机制解析:FASTCALL直传指针打破性能瓶颈 开发者普遍依赖通用元组打包处理C扩展参数,但在高频调用场景下需采用METH_FASTCALL直接传递原生指针数组以消除元组创建与解包开销。 视频系统性地解析了Python C扩展中定义类方法的核心结构体`PyMethodDef`及其字段含义,重点阐释了末尾哨兵的作用与`.pyi`存根文件的生成机制。随后,作者对比了位置参数模式`METH_VARARGS`配合`PyArg_ParseTuple`的解析方案,以及`METH_NOARGS`、`METH_O`和`METH_VARARGS | METH_KEYWORDS`等贴合不同入参场景的高效选项。最后,针对Profiler等高频调用的极端性能需求,视频介绍了`METH_FASTCALL`如何通过消除元组打包开销来实现极致性能优化。 Python C 扩展通过 PyMethodDef 结构体数组注册类方法,末尾必须包含全为 NULL/0 的哨兵元素以弥补 C 语言数组传参丢失长度信息的缺陷。 (0:00) 通用传参标志 METH_VARARGS 配合 PyArg_ParseTuple 可以解析位置参数,但在高频回调场景下会产生创建元组和动态解包的性能开销。 (3:40) 针对不同传参场景,Python C API 提供了 METH_NOARGS、METH_O 以及按位或组合的 METH_VARARGS | METH_KEYWORDS,能分别优化零参、单参及位置加关键字传参的处理逻辑。 (6:40) cProfile 等高频调用的性能敏感模块采用 METH_FASTCALL 机制,直接传递 C 原生指针数组和长度,彻底消除临时元组打包的性能开销。 (10:00)

Reading summary

码农高天指出,编写 Python C 扩展的核心难点在于面向 Python 暴露方法时的接口规范。通过理解 `PyMethodDef` 的结构以及各类 `METH_` 传参标志的机制,开发者能够针对不同调用场景选择最合适且高效的参数解析方案。 C 扩展方法注册与 PyMethodDef 结构 (0:00) 在 C 扩展开发中,自定义类的方法统一注册在 `PyTypeObject` 的 `tp_methods` 字段中。该字段是由 `PyMethodDef` 结构体组成的数组,包含四个成员:Python 调用时的方法名(`ml_name`)、强转为 `(PyCFunction)` 的 C 函数指针(`ml_meth`)、传参标志位(`ml_flags`)以及文档字符串(`ml_doc`)。由于 C 语言数组传参时退化为指针并丢失长度信息,数组末尾必须以 `{NULL, NULL, 0, NULL}` 作为哨兵(Sentinel)来标识结尾。日常在 IDE 中按住 Ctrl 点击跳转到的 `.pyi` 存根文件,正是利用类型签名和省略号暴露了这些底层由 C 扩展实现、源码无法直读的接口。 位置参数与格式化解析工具 (3:40) 对于仅支持位置参数(如签名末尾带 `/` 的 Positional-only 参数)的函数,应声明 `METH_VARARGS` 标志位。底层 C 函数固定接收 `self` 与装有所有位置参数的元组 `args`。在 C 语言内部,开发者通常调用 `PyArg_ParseTuple` 并传入格式化字符串来提取参数,例如用 `O` 提取为 `PyObject*`,用 `s` 转换为以 null 结尾的 C 字符串指针 `char*`。码农高天以开源项目 VizTracer 的实践说明,虽然很长一段时间该项目只用这一种接口配合外层 Python 校验,但统一用位置元组打包并不在所有场景下都最优。 特化参数标志与关键字传参组合 (6:40) 除了通用的位置参数模式,Python C API 还针对特定输入形式提供了更简洁的选项。对于不接收参数的方法,使用 `METH_NOARGS` 可让 Python 运行时自动校验并拦截非法传参,未使用的 C 形参可用 `Py_UNUSED` 宏消除警告;严格只接收一个参数时,`METH_O` 允许直接传入单一 `PyObject*`,免去了元组打包与解包的开销。当函数需要支持关键字传参时,可通过按位或组合为 `METH_VARARGS | METH_KEYWORDS`。此时 C 函数扩展为接收 `self`、`args`(元组)与 `kwargs`(字典)三个参数,解析时须使用 `PyArg_ParseTupleAndKeywords` 并提供以 NULL 结尾的静态字符串数组(`kwlist`)标识键名;若包含可选参数,可在格式字符串中加入 `|` 标记,未传时变量将保留预赋的初始默认值。 绕过元组打包的极速调用:METH_FASTCALL (10:00) 在性能分析器等需要高频触发回调的敏感场景下(如标准库中的 `cProfile`),常规传参创建和解析元组的开销会成为瓶颈。CPython 提供的 `METH_FASTCALL` 机制绕过了临时元组的构建,直接向 C 函数传入原生指针数组与长度(`(PyObject *self, PyObject *const *args, Py_ssize_t nargs)`)。由于脱离了元组容器,开发者无法使用 `PyArg_ParseTuple`,必须通过索引手动取值。该模式同样可以配合 `METH_KEYWORDS` 使用,以极低的调用代价实现极致性能。 对于同时熟悉 Python 与 C 的开发者而言,掌握 `METH_VARARGS`、`METH_O`、`METH_KEYWORDS` 及 `METH_FASTCALL` 等标志的映射机制,能够彻底扫除 C 扩展接口层的调用壁垒,并在易用性与极致运行效率之间做出合理的架构权衡。

精炼阅读摘要

码农高天在程序中指出,编写 Python C 扩展的核心难点常在于 Python 与 C 之间的接口层规范,合理选择 `METH_` 传参标志能够优化方法签名并显著降低高频调用下的开销。 C 扩展方法注册与 PyMethodDef 骨架 (0:00) 在 Python C 扩展中定义类时,自定义方法统一注册在 `PyTypeObject` 的 `tp_methods` 字段中。该字段是一个由 `PyMethodDef` 结构体组成的数组,包含四个关键元素:供 Python 调用的方法名 `ml_name`、强转为 `(PyCFunction)` 的 C 函数指针 `ml_meth`、定义调用规范的传参标志 `ml_flags`,以及文档字符串 `ml_doc`。由于 C 语言数组传参退化为指针后会丢失长度信息,数组末尾必须以 `{NULL, NULL, 0, NULL}` 作为哨兵元素,告知 CPython 运行时数组在此终结。日常在 IDE 中点击方法跳转到的 `.pyi` 存根文件,其底层实现在 C 扩展中,该文件通过保留类型签名与省略号为类型提示和自动补全提供支持。 位置参数与 PyArg_ParseTuple 解析 (3:40) 针对仅接收位置参数(即类型签名中带有斜杠 `/` 的仅限位置参数)的方法,需要声明 `METH_VARARGS` 标志位。此时底层 C 函数固定接收 `PyObject *self` 和装有所有位置参数的 Python 元组 `PyObject *args` 两个参数。C 语言内部普遍通过调用 `PyArg_ParseTuple` 来解析此类参数,配合格式化字符串将元组中的值按序提取至 C 变量中。例如格式化字符 `O` 表示提取原生的 `PyObject*` 对象指针,而 `s` 则会自动将 Python 的 `str` 转换为空字符结尾的 C 字符串指针 `char*`。码农高天提到其开源项目 VizTracer 曾长期使用这种单一接口,尽管在外层包裹 Python 校验再统一传入 C 层的做法可行,但并非所有场景下的最优选择。 针对固定参数与关键字的高效匹配 (6:40) 除了打包元组,Python C API 还针对特定调用场景提供了专用标志。不需要任何参数的方法可以直接使用 `METH_NOARGS`,Python 运行时会自动拦截传参并报错,C 函数的第二个无用参数可配合 `Py_UNUSED` 宏消除编译器告警;对于严格只接收一个参数的方法,使用 `METH_O` 可以将该参数直接作为 `PyObject*` 传入 C 函数形参,避免了元组的创建与解包开销。对于同时支持位置和关键字参数的场景,可通过按位或组合为 `METH_VARARGS | METH_KEYWORDS`,此时 C 函数扩展为三个参数(包含 `kwargs` 字典),并需调用 `PyArg_ParseTupleAndKeywords` 配合以 NULL 结尾的静态关键字字符串数组进行解析。若参数支持缺省,可在格式字符串中加入竖线 `|`,竖线后的参数在未传入时将保留 C 变量的预设初始值。 极致性能需求下的 METH_FASTCALL 机制 (10:00) 在以 `cProfile` 为代表的性能敏感工具中,高频触发的回调函数难以承受普通传参所伴随的临时元组创建、垃圾回收以及 `PyArg_ParseTuple` 解析开销。为了实现零打包开销,CPython 提供了 `METH_FASTCALL` 机制。在 `Fastcall` 模式下,C 函数签名调整为接收原生指针数组 `PyObject *const *args` 与参数个数 `Py_ssize_t nargs`,开发者必须手动通过数组索引提取参数。当与 `METH_KEYWORDS` 组合使用时,传入的关键字信息也将由元组记录。该机制大幅提升了调用速度,专门适用于参数结构简单但触发极其频繁的底层性能瓶颈代码。 理清 `PyMethodDef` 中各类 `METH_` 传参标志与对应的解析方法,是消除 C 扩展接口层使用门槛的关键;开发者需在通用易读的参数打包解析与极致性能的底层调用机制之间做出合理权衡。

Detailed notes — zh

## Background 该视频由 YouTube 频道“码农高天”于 2024 年 11 月 16 日发布。视频旨在厘清 Python C 扩展(C Extension)中定义面向 Python 方法时的参数含义、对应不同调用形式的标志位,以及在 C 语言层解析 Python 参数的具体实现细节。 ## TL;DR 本视频深入剖析了 CPython 中自定义方法如何通过 `PyMethodDef` 结构体挂载到类的 `tp_methods` 数组中,并重点解析了不同方法标志位(flags)的机制与应用场景。高天详细对比了处理通用位置参数的 `METH_VARARGS` 与 `PyArg_ParseTuple` 解析模式、免参数和单参数优化标志 `METH_NOARGS` 与 `METH_O`,以及支持关键字参数的 `METH_VARARGS | METH_KEYWORDS`。此外,他还结合底层 Profiler 场景介绍了避免元组打包开销、以指针数组直接传参的极致性能方案 `METH_FASTCALL`,帮助开发者彻底打通 C 扩展接口定义的关键瓶颈。 ## Atmosphere & style 视频展现了硬核而从容的技术极客氛围,讲解风格严谨细致、条理清晰。高天结合自己维护开源项目 VizTracer 的实战经验和标准库底层源码,将晦涩的 CPython 底层宏定义与接口规范抽丝剥茧地娓娓道来,既有实操层面的避坑提示,又有原理层面的权衡考量,带给观众一种豁然开朗的透彻感。 ## Bulletin - [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) Python C 扩展通过 tp_methods 数组暴露自定义方法,每个方法由 PyMethodDef 结构体定义,末尾必须包含一个全空哨兵元素以通知解释器数组结束。 - [06:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=360s) METH_VARARGS 与 METH_KEYWORDS 通过按位或组合支持位置和关键字参数,并配合 PyArg_ParseTupleAndKeywords 及 kwlist 数组完成 C 侧参数自动解析。 - [09:44](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=584s) CPython 针对性能敏感场景提供了 METH_FASTCALL 标志位,直接传递 C 指针数组和参数长度,彻底避免了 Python 元组打包的开销。 ## Key takeaways - [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) 在 C 扩展中定义类时,内置魔术方法有专属槽位,而自定义方法均注册在 tp_methods 数组中。 - [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) PyMethodDef 结构体包含方法名、C 函数指针、传参标志位 ml_flags 和文档字符串四个核心字段。 - [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) C 语言数组在传参时会丢失长度信息,因此 tp_methods 数组末尾必须以 {NULL, NULL, 0, NULL} 哨兵元素结尾。 - [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) Python 的 .pyi 类型存根文件中仅包含签名与省略号的方法,底层通常完全由 C 扩展实现。 - [03:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=180s) 在 Python 函数签名中,斜线 / 表示其前面的参数只能作为纯位置参数传递,严禁使用关键字调用。 - [03:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=180s) METH_VARARGS 对应只接收位置参数的模式,C 函数接收 self 和一个 args 元组对象。 - [03:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=180s) 使用 PyArg_ParseTuple 解析参数时,格式化字符 'O' 保持 Python 对象引用,'s' 会自动转换为 C 字符串 const char*。 - [06:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=360s) METH_NOARGS 用于无参方法,Python 会自动校验参数数量,C 函数使用 Py_UNUSED 宏避免未使用变量警告。 - [06:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=360s) METH_O 专用于仅接收单个参数的方法,参数会作为 PyObject* 直接传入,无需经过打包和解包元组的过程。 - [06:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=360s) 传参标志位本质上是基于位移定义的掩码,因此可以使用按位或(|)组合支持复杂传参形式。 - [06:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=360s) PyArg_ParseTupleAndKeywords 依赖以 NULL 结尾的静态字符串数组 kwlist 来将关键字参数映射到对应位置。 - [06:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=360s) 在格式化字符串中使用竖线 | 可以标记后续参数为可选参数,若调用端未传参则目标变量保持默认值。 - [09:44](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=584s) cProfile 等分析器的回调函数对调用开销极其敏感,因此底层广泛采用 METH_FASTCALL 机制优化性能。 - [09:44](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=584s) METH_FASTCALL 采用指针数组 (PyObject *const *args) 和参数个数 (nargs) 传参,以牺牲 ParseTuple 自动解析的便利性换取极致性能。 ## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=0s) C 扩展中的方法定义结构与 `PyMethodDef` 在编写 Python 的 C 扩展(C Extension)时,很多人第一次往往是“照葫芦画瓢”,要么直接抄官方文档,要么参考别人现有的扩展,现在有了 ChatGPT 更是一键生成框架。搭建出一个能用的 C 扩展架子并不难,但其中核心的接口(Interface)细节——即如何在 C 扩展中定义供 Python 调用的方法、各种传参标志位的含义以及在 C 语言中如何解析参数——往往容易让人感到困惑。在 C 扩展中建立一个类,内置方法或魔术方法(如 `__new__` 和 `__init__`)会有固定的槽位(Slots),而所有用户自定义的方法都统一存放在类的 `tp_methods` 数组中。 ```mermaid flowchart LR accTitle: PyMethodDef 结构组成 accDescr: 展示 PyMethodDef 结构体的四个字段及结尾的哨兵元素 subgraph Def ["PyMethodDef 结构体"] F1["ml_name<br>(Python 中的方法名)"] F2["ml_meth<br>(C 函数指针,强转为 PyCFunction)"] F3["ml_flags<br>(传参标志,如 METH_VARARGS)"] F4["ml_doc<br>(方法的 Docstring 文档)"] end Sentinel["Sentinel 哨兵元素<br>{NULL, NULL, 0, NULL}"] Def --> Sentinel ``` `tp_methods` 是一个由 `PyMethodDef` 结构体组成的数组,每一个结构体代表一个暴露给 Python 的自定义方法,包含四个核心部分: 1. **方法名(`ml_name`)**:在 Python 端调用该方法时使用的名字。 2. **C 函数实现(`ml_meth`)**:该方法在 C 语言中对应的函数指针。在代码中通常会将其强制转换为 `(PyCFunction)` 类型,这是一种指针层面的类型转换,核心目的是消除编译器警告。 3. **方法描述/传参标志(`ml_flags`)**:本期重点,决定了 Python 在调用该方法时如何传递参数。 4. **文档字符串(`ml_doc`)**:该方法的 Documentation。 在该数组的最末尾,必须加上一个全空的元素作为“哨兵”(Sentinel),即 `{NULL, NULL, 0, NULL}`。高天解释道:“因为我们知道 C 的 array 在进行传参的时候它会丢失掉长度这个信息对吧,那这个 Sentinel 就是常见的告诉你最后一个已经到了的方式。”哨兵的存在是为了告知 CPython 解释器方法定义到此结束。 为了直观展示 C 方法在 Python 侧对应的接口形式,高天以一个 `.pyi` 类型存根文件(stub file)为例进行说明。平时在 VS Code 等编辑器中按住 Ctrl 顺着函数调用一直点进去,常会跳转到只有签名(signature)而函数体全是 `...`(Ellipsis)的文件,这往往意味着该类的底层实现完全是用 C 编写的。开发者看不到 C 源代码,但通过 `.pyi` 文件可以获知其接口签名与传参规则,方便 IDE 进行静态分析和类型提示(Type Hinting)。 ### [03:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=180s) 位置参数模式:`METH_VARARGS` 与 `PyArg_ParseTuple` 以一个典型的底层追踪函数 `thread_trace_func` 为例,该函数用于传给 `sys.setprofile`。在纯 CPython 驱动的内部调用场景下,它的签名定义为只接受三个位置参数(Positional Arguments),签名末尾带有斜线 `/`(如 `def thread_trace_func(frame, event, arg, /):...`),表示斜线之前的参数严禁以关键字参数(Keyword Arguments)形式传递。调用者只能按顺序传入参数,不能写成 `frame=...` 的形式。 ```mermaid flowchart TD accTitle: PyArg_ParseTuple 解析流程 accDescr: Python 位置参数元组通过 PyArg_ParseTuple 解析映射到 C 变量 A["Python 调用参数: args (PyObject* tuple)"] --> B["PyArg_ParseTuple(args, 'OsO', &frame, &event, &trace_args)"] B --> C["'O' -> PyObject* frame"] B --> D["'s' -> const char* event (转换成 C 字符串)"] B --> E["'O' -> PyObject* trace_args"] ``` 针对这种纯位置参数的 Python 函数,C 扩展中配置的 flag 为 `METH_VARARGS`。此时对应的 C 函数签名接收两个参数: 1. `PyObject *self`:对应 Python 中的实例自身。 2. `PyObject *args`:一个由所有位置参数构成的 Python 元组(tuple)。 为了将元组中的参数提取到 C 变量中,通常使用 `PyArg_ParseTuple` 函数。其参数格式化字符串定义了提取的目标类型: - **`O`**:代表 `Object`,即原封不动地保持 Python 的 `PyObject *` 引用。 - **`s`**:代表 `string`,将 Python 的字符串对象提取并转为 C 语言的 `const char *`。 例如格式化字符串 `"OsO"`,就代表将 `args` 中的第一个参数作为 `PyObject*` 存入 `frame`,第二个参数转为 `char*` 存入 `event`,第三个参数作为 `PyObject*` 存入 `trace_args`。 高天提到,这种位置参数模式具有非常强的普适性:“实话跟大家说 VizTracer 在很长一段时间内都是这么干的,就只有这一种 interface 的方式,这样确实在很多场景下都够用了,但是它显然不是最好的方法,也不是最方便的方法。”开发者完全可以在 C 扩展外层包裹一层 Python 代码去检查参数,确认无误后再以纯位置参数调入 C 扩展,但 CPython 本身提供了更原生、更高效的参数标志位。 ### [06:00](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=360s) 特殊参数标志:`METH_NOARGS`、`METH_O` 与 `METH_VARARGS | METH_KEYWORDS` 除了通用的位置参数元组,Python 还针对常见场景做了针对性优化: | 标志位(Flags) | Python 端调用特征 | C 函数接收参数 | 参数提取/处理机制 | |:--- |:--- |:--- |:--- | | `METH_NOARGS` | 无参数调用,如 `func()` | `(PyObject *self, PyObject *Py_UNUSED(ignored))` | Python 内部自动校验参数个数,若传入参数会报错;C 函数无需手动解析。 | | `METH_O` | 仅接收单个参数,如 `func(x)` | `(PyObject *self, PyObject *arg)` | 唯一的参数直接转为 `PyObject *` 传入,无需构造元组解包。 | | `METH_VARARGS \| METH_KEYWORDS` | 支持任意位置与关键字参数,如 `func(a, b=1)` | `(PyObject *self, PyObject *args, PyObject *kwargs)` | 使用 `PyArg_ParseTupleAndKeywords`,配合参数名数组解析。 | 在 `METH_NOARGS` 下,C 函数为了符合 `PyCFunction` 的调用约定,第二个参数仍需保留,但通常用宏 `Py_UNUSED` 来修饰形参名称,以防止编译器报告未使用的变量警告。 对于同时支持位置和关键字传参的方法,标志位写作按位或组合:`METH_VARARGS | METH_KEYWORDS`。高天解释道:“因为这些 flags 的本质都是一左移某个数,所以可以互相 or 起来。”此时 C 函数拥有三个入参:`self`、位置参数元组 `args` 和关键字参数字典 `kwargs`。解析此结构需使用 `PyArg_ParseTupleAndKeywords`: - 需要额外传递一个以 `NULL` 结尾的静态字符串数组(如 `static char *kwlist[] = {"name", "args", NULL};`),指明每个位置对应的关键字名称。 - 格式化字符串规则与之前一致(如 `"OO"` 分别提取为两个 `PyObject*`)。 - 解释器会根据 `kwlist` 自动匹配关键字字典中的 key,并将对应的 value 填入相应的位置。 如果参数中包含可选参数(Optional Arguments),可以在格式化字符串中使用竖线 `|`,例如 `"|l"`。竖线表示其后的所有参数都是可选的;`l` 表示将其转换为 C 的 `long` 类型。如果 Python 传入了可选参数,目标变量会被更新;如果没传,则保持初始默认值不变。如果去掉 `|` 只写 `"l"`,在调用方没有传递该参数时解释器便会抛出异常。 ### [09:44](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=584s) 极致性能调用:`METH_FASTCALL` 与底层 Profiler 场景 针对需要极高调用性能、无法承受过多参数打包开销的特殊场景,高天介绍了 `METH_FASTCALL`。虽然 VizTracer 中未直接采用此方案,但在标准库的 `cProfile` 模块中有着典型应用。`cProfile` 的底层是一个纯 C 实现的 profiler,profiler 的回调函数(callback)对调用延迟和开销极其敏感,高天强调:“大家知道 Profiler 的 callback 对性能是非常敏感的,他肯定是希望用最小的代价来完成这个调用,所以 callback 调用方法就使用了 FASTCALL。” ```mermaid flowchart TD accTitle: FASTCALL 传参机制对比 accDescr: 传统 METH_VARARGS 元组打包与 METH_FASTCALL 指针数组直接传递的对比 subgraph Standard ["传统 METH_VARARGS"] A1["Python 调用参数"] --> B1["打包成 Python Tuple 对象"] B1 --> C1["传递给 C: (self, args_tuple)"] C1 --> D1["通过 ParseTuple 逐个解包"] end subgraph Fastcall ["极致性能 METH_FASTCALL"] A2["Python 调用参数"] --> B2["不打包为 Tuple,直接构成指针数组"] B2 --> C2["传递给 C: (self, PyObject* const* args, Py_ssize_t nargs)"] end ``` `METH_FASTCALL` 的调用特征与实现细节如下: - **函数签名**:C 侧函数签名通常为 `(PyObject *self, PyObject *const *args, Py_ssize_t nargs)`。 - **避免元组开销**:解释器不会在调用前为位置参数额外分配并打包一个 `tuple`,而是直接将参数以底层 C 指针数组的形式传递,并附带参数数量 `nargs`。 - **权衡取舍**:获得了极致的运行性能,但牺牲了解析的便利性,无法再简单调用 `PyArg_ParseTuple` 自动完成类型检查和解包,通常适合参数结构相对简单、对吞吐要求严苛的场景。 - **关键字扩展**:`METH_FASTCALL` 同样可以与关键字标志组合为 `METH_FASTCALL | METH_KEYWORDS`,此时 C 函数将额外接收一个包含关键字信息的参数,兼顾调用灵活性与运行效率。 对于一个 Python 和 C 语言都有一定基础的开发者而言,编写 C 扩展最容易让人望而却步的就是接口定义这一层。搞清楚这些标志位的对应机制,在阅读 CPython 源码以及编写高性能扩展库时就能更加游刃有余。 ## Notable quotes > [09:44](https://www.youtube.com/watch?v=FkvUcWnDgXk&t=584s) Profiler 的 callback 对性能是非常敏感的,他肯定是希望用最小的代价来完成这个调用,所以 callback 调用方法就使用了 FASTCALL。 — 高天 ## Mentioned 高天, Python, CPython, ChatGPT, VS Code, VizTracer, cProfile ## Video description 可能有很多人并不知道Python的C extension里,当我们定义一个面向Python的方法的时候,给的这个参数是什么意思,分别对应了哪种Python的调用形式,以及在C语言中如何获取Python参数。这期视频我们来好好聊聊这个细节。 [Source](https://www.youtube.com/watch?v=FkvUcWnDgXk)

Source quotes

[9:44] 高天: Profiler 的 callback 对性能是非常敏感的,他肯定是希望用最小的代价来完成这个调用,所以 callback 调用方法就使用了 FASTCALL。

Original transcript

[0:00] 今天我们来聊有跟C Extension有关的话题就是c extension中一个类的方法应该怎么写我想大部分人可能和我一样在第一次写C Extension的时候都是赵胡罗话表你可能照着官方文档抄了一个,也可能参考了一下别人已经写好的extension。当然现在有了拆GPT这件事就变得更方便了直接问拆GPT就行了所以像这样搭出来一个C extension的框架并且能使用其实挺简单的那我们今天只聊其中的一个细节就是Interface 这个部分怎么在c extension里面去写这个python可以调用的方法在大部分情况下我们的C extension里会建立一个新的类和我们正常写python的时候一样,当然你这个module本身也可以有方法,这个不是非常重要啊,我们就拿类来举例子。当我们建立了这样一个类的时候它有一些内置方法或者说我们平时说的魔术方法是有固定的位置的比如说这个类的new和init这些东西不是我们今天的重点我们主要来讲那些自定义的方法所有的自定义的方法都在这个.tp methods里也就是说这个TP methods里就是你这个类所有的自定义的接口那我们看一下这个tracer methods这个tracer methods就是一个数字每一个element都是一个py method def每一个PiMethodF由四部分构成第1个部分是这个方法的名字也就是在拍当中应该使用哪个名字来调用这个方法第二个部分是这个方法对应的C的函数也就是这个方法的实现这里注意一下我们会把这个方法cast到pyc function这是一个pointer level的cast它的核心目的是让Compiler不要出Warning第三个部分是这个方法的描述这也是等等我们要讲的重点他主要决定了在python调用这个方法的时候是怎么传餐的最后一个部分是这个方法的Documentation在整个数组的最后我们一定要记得加上一个全空的element这个东西就叫Sentinel 哨兵他是用来告诉Pyta到这儿我的所有的定义就结束了因为我们知道C的array在进行传餐的时候它会丢失掉长度这个信息对吧那这个Sentinel就是常见的告诉你最后一个已经到了的方式我们这期视频的核心是讲这些flags因为我们知道Python函数传参的方式和C传参的方式是非常不一样的为了让大家更容易理解我这里还给大家做了一个这个类的signature.pyi文件这另外说一嘴啊,大家有时候在VS Code里面按着Ctrl,点啊点啊点,就点到了一个类似这样的文件里他有所有方法的signature啊也就是他怎么传餐的但是实现呢都是三个点很多人不知道这是怎么回事啊这个就是它实际的类的实现是在C里面完成的你没有办法直接看到他这个类的原代码但是他把这个类的interface 他把这个类所有的方法都展示给你了 告诉你应该如何调用主要是为了辅助TypePin跟IDE这些东西的那我们先从一个比较典型的函数说起就是这个Thread Trace Funk这个函数是我可能要给sys.setProfile的也就是说正常使用的情况下它只会为Cpython本身调用因此我这里signature给的是三个positional argument注意最后的斜线他表示之前的这些argument都不能以keyword的形式传进来也就是说这个函数调用的时候你必须要写threadtracefunk然后参数参数参数而不能写比如Threat Trace Funkframe 等于什么 event 等于什么arg 等于什么这样是不行的那对于这种所有参数都以Positional的形式传进来的Python函数我们对应的flags是var args这个Math Var Args决定了Python在调用你这个C函数的时候给的参数是什么样的我们看一下对应的C函数他有两个参数第一个是Object,第二个是Args那第一个这个object对应的就是self 也就是这个instance本身 而后面的那个args是一个tuple这个tuple里面是所有以positional形式传进来的参数那对于这种形式的参数我们一般用这个pyArgParseTupel来拿到里面的东西前面这个ARGS就是这个TUPLE中间这个OSO它代表了我从这个TUPLE里面想拿到什么样的参数O这里最常见的就是Object也就是拿出来之后它还是一个object我们知道在C extension里面所有的python的object都可以表示为py object这个O就是原封不动拿出来而S表示把这个object给我变成一个stress [5:00] 大家看我接收这个参数的变量event它是一个char星它会指向一个cStream后面就是分别把这个oso保存到哪里整个这个parseTupel的意思就是把args这个tuple里面第一个object还以object形式保存到frame里第二个object变成string保存到event里第三个object还以object形式保存在这个traceArgs里这样我们就完成了Python参数到C函数里面这个参数的转换那我们之所以先讲这个方法是因为大家可以发现这个方法其实是有普世性的它就是把Python传进来的参数一个一个拿出来嘛所以大家可能会发现你写的那个C extension只要有这一个方法好像就够了你大不了可以在你那个C extension外面再套一层函数去检查这个参数然后确保参数对了之后在永远以positional的形式传进来实话跟大家说VizTracer在很长一段时间内都是这么干的就只有这一种interface的方式这样确实在很多场景下都够用了但是它显然不是最好的方法也不是最方便的方法所以今天我们再给大家介绍一些其他常用的参数类型首先像这种不需要参数的方法其实还挺常见的那我们当然可以也写成这样我就压根不 parse这个 args了或者我检查一下这个 args里面是不是空的硬盲吗但是呢对于这种方法Python提供了一个专门的标签就是这个math no args你可以直接在定义这个方法的时候告诉他我这个方法本身就是不接受任何argument的这样python在内部会给你自己检查是不是有argument被传进来了但是大家注意一下这个函数一般情况下还是要写成这种两个参数的形式只不过第2个参数我们一般会写成unused这里我们会用这个py unused macro修饰一下他是为了防止compiler warning因为这个参数没有用到大家看这么一些是不是参数处理的部分就变得更简单了都让python去做了我们什么都不用干还有一个情况非常常见就是这个方法呀它只有一个参数那这样的方法我们一般使用的是math oo代表object就是python在调用这个c函数之前会把这唯一的参数给转变成object传进来我们看这里第2个参数就可以直接写成这个object的形式了我们不需要去检查参数的数量也不用自己手动把那个tuple的第一个拿出来变成这个object这也是一个非常方便的用法那还有一种非常普适的就是这种非常python的参数形式你既可以用positional也可以用keywords你可以直接巴巴两个参数一甩也可以写name等于什么args等于什么那对于这种有一点这个新args新新 keyword args的参数形式我们的这个flags就要写成math var args or math keywords就是这两种参数都存在这两个flags是用or连接起来的因为这些flags的本质都是一左一某个数所以可以互相or起来那对于这种传参形式对应的这个c函数就会有三个参数除了self跟args之外还多了一个keyword其中这个args还是那个tuple而keyword是一个dictionary我们读取参数的时候就稍微复杂一点了我们要用这个pyarg parse tuple and keywords前两个参数分别是args和keyword同时我们要给一个keyword list这个list的意思是这个位置又叫什么名字我们看刚才那个我们想设计的函数第一个位置它又叫name对吧第二个位置又叫args对吧所以这里呢就是name args后面那个now也是sentinel你可以把它简单的理解为如果我keyword这个字典里面有name这个key的话我就把这个key对应的value放到index为0的那个地方args放到index为1的地方中间这个OO跟刚才讲的一样他俩我都把它直接parse成PythonObject最后就是把这两个东西保存到哪这个就跟之前差不多那可能还有一种情况就是这个参数是可选的你可以给也可以不给这个时候我们刚才的那个flags还是该怎么着怎么着有keyword要写上keyword没有keyword就用这个vararx但是在我们parse的时候注意一下我们这个stream里是一个竖线也就是or L后面的L代表long也就是我要把传进来的这个object给变成C里面long的形式前面那个竖线或者是or它代表从此刻开始往后的所有参数都是可选的如果给了就写没给就拉倒所以对于这个inputPid [10:00] 如果ARGS里面给了这个参数他就会把它转化为 long 然后保存到 input pid 里如果二个词里面没给那input_pid就不会被修改如果没有这个沃尔这里面只有一个L的话那二个词里面如果什么都没有它是会报错的 说你这个参数不对好那最后再给大家介绍一种有可能你会用得着的情况在V-Treator里面没有出现啊但是有的时候你希望我这个调用特别特别特别快就这个调用越快越好啊你别给我整的没用的比如说在C profile这个module里它的底层是一个C写的profiler大家知道Profiler的callback对性能是非常敏感的他肯定是希望用最小的代价来完成这个调用所以callback调用方法就使用了fastcallFascal对应的C函数的参数形式是这样的第1个参数还是self但后面两个参数分别是PyObjectArgs和一个size那这种形式本质上就是他没有把这些argument给你打包成一个tuple他直接给你扔进来了这样在性能上它就是最好的当然你就没有那些非常方便的parse的手段了一般在你参数比较简单的时候可能比较适用然后Fastcall也是支持keyword argument的你在这个fastcall后面or一个math keyword就可以它的那个C函数就额外有一个keyword的参数和前面讲那个keywords差不多c extension里面的东西还是比较多的但是我觉得对于一个python跟c本身写的都还不错的人来说可能刚开始相对最难的部分就是接口的这个部分也就是怎么从python里面调用c希望今天这期视频对大家有所帮助好了这一边是李,喜欢的感谢关注,别忘了一件,再见了哦!优优独播剧场——YoYo Television Series Exclusive