1. 前言
Python 作为一种功能强大、简洁易学的编程语言,在人工智能领域发挥着至关重要的作用。 当我们讨论Python时,首先需要明了的是Python只是一个接口。有一个关于Python应该做什么以及怎么做的具体说明(就像其他任何接口一样),并且对应的有很多具体的实现(也像其他接口一样)。 目前主流的Python实现有:CPython(基于C),JPython(基于Java),IronPython(基于 .NET),RubyPython等等。 这么多不同语言的实现的意义是什么?:可以更好的和其他语言glue起来,比如JPython:
[Java HotSpot(TM) 64-Bit Server VM (Apple Inc.)] on java1.6.0_51>>> from java.util import HashSet>>> s = HashSet(5)>>> s.add("Foo")>>> s.add("Bar")>>> s[Foo, Bar]NOTE本文基于CPython版本3.12.4
2. CPython的架构
2.1 代码的流动过程
你的代码从遇到到执行,其中经历了什么?python代码的执行需要经过一下流程: Python源码 -> Scanner -> Parser -> Compiler -> Code Evaluator
2.2 语法规范
我们从语法开始分析一门语言。CPython 3.9起使用PEG(Parsing Expression Grammar)进行语法/词法分析。
Python的语法被定义在了Grammar/python.gram文件中,而token则被定义在了Grammar/Tokens中,举个例子:
LPAR '('RPAR ')'LSQB '['RSQB ']'LBRACE '{'RBRACE '}'事实上,你可以修改一下这个文件,创建一个属于你自己的Python版本(比如说()⬅这个作为list,[]⬅这个作为tuple) 然后重新生成peg:
regen-pegen: @$(MKDIR_P) $(srcdir)/Parser @$(MKDIR_P) $(srcdir)/Parser/tokenizer @$(MKDIR_P) $(srcdir)/Parser/lexer PYTHONPATH=$(srcdir)/Tools/peg_generator $(PYTHON_FOR_REGEN) -m pegen -q c \ $(srcdir)/Grammar/python.gram \ $(srcdir)/Grammar/Tokens \ -o $(srcdir)/Parser/parser.c.new $(UPDATE_FILE) $(srcdir)/Parser/parser.c $(srcdir)/Parser/parser.c.new2.3 输入Reader
现在你已经了解了Python的语法,是时候探索一下代码是如何进入可执行状态的了。 在CPython 中运行Python代码的方式有很多,下面是一些最常用的方法:
- 通过
python -c命令加一个Python字符串来运行 - 通过
python -m命令加一个模块名称来运行一个模块 - 通过
python <file>运行,其中<file>为文件的具体路径,并且文件中包含Python代码 - 通过标准输入将 Python 代码通过管道传输到可执行文件中,例如
cat <file> | python - 通过启动交互式解释器(REPL)并一次执行一个命令
- 使用C API,并用Python作为嵌入式环境
为了执行python代码,解释器需要三个部分:
- 一个要执行的模块;
- 用于保存变量等信息的状态;<------环境变量,运行参数等等
- 配置项,例如开启了哪些选项。<-----编译选项
有了这三个组件,解释器就可以执行代码并且输出:
注意,python执行的总是模块,任何代码在被执行之前,必须从输入编译成一个模块。
正如之前所讨论的那样,输入的类型可以有多种:
- 本地的文件和包;
- 输入/输出流,如:标准输入或内存管道;
- 字符串。
2.3.1 读取文件和输入
一旦CPython有了运行时配置和命令行参数,它就可以加载它所需要执行的代码,这个任务由Modules/main.c文件中的pymain_main()函数来完成。
命令行输入字符串
CPython 可以通过指定-c选项,从而通过命令行模式来执行一个小的Python应用,比如:
$ ./python -c "print(2 ** 2)"4首先,pymain_run_command()函数在Modules/main.c文件中被执行,其将命令行中通过-c传入的命令行作为一个wchar_t*类型的参数。完成此操作后,pymain_run_command()将会把Python字节对象传递给 PyRun_SimpleStringFlags()用于执行,它将一个字符串转换成Python模块,然后把它送去执行。
2.3.2 本地模块输入
执行 Python 命令的另一种方式是通过-m选项与模块名一起使用.
CPython导入一个标准库模块runpy并通过PyObject_Call()来执行它,导入的过程由一个名为PyImport_ImportModule()的C API函数完成。
为了运行目标模块,runpy 做了以下三件事:
- 为你指定的模块名调用
import()方法; - 将 name(模块名称)设置到名为
main的命名空间; - 在
main命名空间中执行模块。
2.3.3 来自脚本文件或标准输入的输入
如果python执行时的第一个参数是一个文件名,例如python test.py,CPython将会打开一个文件句柄并将句柄传递给PyRun_SimpleFileExFlags(),这个方法可以处理三种类型的文件路径:
- 如果文件是
.pyc文件,那么它将会调用run_pyc_file(); - 如果文件是脚本文件
.py,那么它将会调用PyRun_FileExFlags(); - 如果文件是
stdin,如:用户执行了<command> | python,那么它会将stdin作为一个文件句柄并调用PyRun_FileExFlags()。
2.4 解析Parser
在上部分中,我们知道了Python如何从各类来源中读取文本。接下来需要将其转换为编译器可以使用的结构,这个过程被称为解析(parsing)。
CPython使用具象语法树(CST)和抽象语法树(AST)两级结构来分析代码。
具体地说,解析过程分为两部分:
- 使用parser-tokenizer创建具象语法树
- 通过具象语法树创建出抽象语法树
其中
parser为2.2中我们生成的Parser/parser.c,tokenizer为Parser/tokenizer.c,parser-tokenizer为Parser/parsetok.c
2.4.1 CST
parser-tokenizer接收文本输入并循环执行分词器和解析器,直到光标到达文本末尾才结束(或者出现了一个语法错误)。
在执行前,parser-tokenizer会先创建出一个tok_state实例,分词器会将所有的状态存放到这个临时的数据结构中。分词器状态包括当前光标位置、行等信息。
parser-tokenizer通过调用tok_get()获取到下一个token。tokenizer将token ID传递给parser,parser使用parser-tokenizer生成的 DFA 在具象语法树上创建节点。
举个例子,比如:将算术表达式 “a + 1” 转换成具象语法树,如下图所示:

2.4.2 AST
下一个阶段是将parser-tokenizer生成的具象语法树转换为能被执行且抽象层次更高的东西。
具象语法树是代码文件中文本的字面表示方式。Python的基本语法结构已经被解释过了,但你无法使用具象语法树来建立函数、作用域或者任何Python语言核心特性。
在代码被编译前,具象语法树需要转换为能表示 Python 实际结构的更高层次的结构。这个能表示具象语法树的结构被称为:抽象语法树(AST)。
比如:在AST中的二元运算操作被称为BinOp并被定义为一种表达式类型。其由三部分构成:
left:运算的左侧部分;op:运算符,如:+、-、*;right:运算的右侧部分。
a+1的AST可以这样表示:
与AST相关的文件有:
Python/ast.c抽象语法树的实现Parser/Python.asdl在领域特定语言ASDL 5中的抽象语法树节点类型和属性列表集合Include/Python-ast.h抽象语法树节点类型声明,由Parser/asdl_c.py文件生成
最终,Parser的输出类型是一个能表示Python模块的mod_ty。
同样的,在这部分我们关心的是执行流程,关于其中的实现细节会放在第三部分中。
2.5 编译Compiler
完成Parser后,解释器拥有了一棵抽象语法树,其包含Python代码的操作、函数、类以及命名空间。
接下来,编译器的任务是将抽象语法树转换为CPU能理解的指令。
有关的文件:Python/compile.c和Python/compile.h
此编译任务会被分成两个组件:
编译器:遍历抽象语法树并创建一个控制流图(CFG),此图表示执行的逻辑顺序;
汇编器:将 CFG 中的节点转换为能按顺序执行的语句,即字节码(bytecode)。
经过Assembler之后,大伙平时(可能)见到过的code object就出现了

首先,我们要先理解几个概念:
整个包装类(容器)被称为编译器状态,其中包含一个符号表;
符号表包含了许多变量名,并且可以选择包含子符号表;
编译器类型包含许多编译器单元;
每个编译器单元可以包含许多名称、变量名、常量以及 cell 变量;
编译器单元还包含许多基础帧块;
基础帧块包含许多指令。
在编译器启动前,会创建出一个全局编译器状态。编译器状态中包含了编译器使用的属性,比如编译器标志、嵌套等级、优化等级、栈、指向__future__模块的指针以及PyArena*(内存分配区域的指针)。它还包括了到其他数据结构的链接,如符号表。
这里不讲__future__模块,因为在座的应该没人在用python2.x了
指令
指令instr具有如下字段:
| Attribute | Type | Description |
|---|---|---|
| i_jabs | unsigned | 指定此跳转为绝对跳转的标志 |
| i_jrel | unsigned | 指定此跳转为相对跳转指令的标志 |
| i_lineno | int | 创建此指令的行号 |
| i_opcode | unsigned char | 此指令表示的操作码编号(参见Include/Opcode.h) |
| i_oparg | int | 操作码参数 |
| i_target | basicblock* | i_jrel 为 true 时指向目标 basicblock 的指针 |
基础帧块
Python/flowgraph.c
基础帧块basicblock包含以下字段:
| Attribute | Type | Description |
|---|---|---|
| b_ialloc | int | 指令数组长度(b_instr) |
| b_instr | instr * | 指向指令数组的指针 |
| b_iused | int | 使用的指令数(b_instr) |
| b_list | basicblock * | 此编译单元中的 block 列表(倒序) |
| b_next | basicblock* | 指向正常控制流到达的下一个 block 的指针 |
| b_offset | int | 块的指令偏移量,由assemble_jump_offsets()计算得到 |
| b_return | unsigned | 如果插入了RETURN_VALUE操作码,则为true |
| b_seen | unsigned | 用于执行基础块的深度优先搜索 |
| b_startdepth | int | 进入块时的堆栈深度,由stackdepth()计算得到 |
编译单元
每个代码块为一个编译单元,当前在操作哪一个unit由compiler_enter_scope()和compiler_exit_scope()进行管理。
/* The following items change on entry and exit of code blocks. They must be saved and restored when returning to a block.*/struct compiler_unit {···};2.5.1 创建符号表
在编译代码之前,complier会创建出一个符号表(symbol table)。
符号表的目的是提供命名空间、全局和局部变量的列表,供编译器用于引用和解析作用域。
与上一章所讲到的 AST 编译类似,PySymtable_BuildObject()在 mod_ty(也就是AST)的可能类型(Module、Interactive、Expression 和 FunctionType)之间切换并访问其中的每个语句,然后递归地探索 AST的节点和分支,并将条目添加到symtable中。
准备好符号表之后,就开始了正式的编译过程。
2.5.2 核心编译过程
现在的PyAST_CompileObject()具有编译器状态、symtable 和 AST 形式的模块,实际的编译过程可以开始了。核心编译器有两个目的:
- 将状态、
symtable和AST转换成控制流图(CFG); - 通过捕获逻辑或代码错误来保护执行阶段免受运行时异常的影响。
AST 模块编译的入口点是compiler_mod()函数,此函数根据模块类型切换到不同的编译器函数,然后循环遍历访问模块中的每个语句。
语句类型是通过调用asdl_seq_GET()确定的, 它会查看AST节点类型,然后调用VISIT宏为每个语句类型调用相应的函数:
小插曲:在这里,编译器用宏做了一个防止表达式作为左值的检查
#define asdl_seq_GET(S, I) _Py_RVALUE((S)->typed_elements[(I)])// Prevent using an expression as a l-value.// For example, "int x; _Py_RVALUE(x) = 1;" fails with a compiler error.#define _Py_RVALUE(EXPR) ((void)0, (EXPR))#define VISIT(C, TYPE, V) \ RETURN_IF_ERROR(compiler_visit_ ## TYPE((C), (V)));比如说,如果当前处理的是一个语句,而语句是for类型,那么compiler_visit_stmt()会调用compile_for()。所有语句和表达式类型都有一个等效的compiler_*()函数。更直接的类型会创建内联字节码指令,而一些更复杂的语句类型会调用其他函数。
简单的说,这里发生了什么呢?compiler_mod()将AST中的节点转换为指令序列,填充到各自的基础帧块中,并将这些基础帧块连接起来,形成CFG(并进行优化)。
2.5.3 汇编过程
一旦这些编译阶段完成,编译器就会拥有帧块列表,每个帧块都包含指令列表和指向下一个块的指针。汇编器(assembler)对基础帧块执行深度优先搜索(DFS),并将指令合并为单字节码序列。
汇编器 API 有一个入口点:assemble(),它具有以下职责:
- 计算内存分配的块数;
- 确保每个边界情况的块都会返回 None;
- 解析任何被标记为相对的跳转语句偏移;
- 调用
dfs()对块执行深度优先搜索; - 向编译器发送所有指令;
- 把编译器状态作为入参调用
makecode()以生成PyCodeObject。
2.5.4 创建Code Object
makecode()的任务是检查编译器状态和汇编器的一些属性,并通过调用PyCode_New()将这些属性放入PyCodeObject中。变量名和常量会作为属性放到Code Object中。
字节码被送到PyCode_NewWithPosOnlyArgs()之前会被送到PyCode_Optimize()。此函数是Python/peephole.c中字节码优化过程的一部分。
窥孔(peephole)优化器检查字节码指令,并在某些情况下用其他指令替换它们。例如,有一个优化器,它可以删除return语句之后的任何无法访问的指令。
2.6 执行Execution
到这里为止,我们已经了解了如何将Python代码解析为抽象语法树并将其编译成Code Object,这些Code Object包含了以字节码形式表示且相互独立的一系列操作。但要执行Code Object还缺少一项关键的内容,它们需要输入。在Python中,输入可能以局部变量或全局变量的形式出现。在本节中,我们将接触到一个名为值栈(value stack)的概念。在Code Object中,字节码操作会在值栈创建、修改并使用变量。
CPython 中执行代码的动作发生在一个核心循环中,这个循环又被称为求值循环 (evaluation loop)。CPython解释器将在该循环中解析并执行由序列化的.pyc文件或由编译器得到的Code Object:
以下是与求值循环相关的源文件:
Python/ceval.c实现求值循环的核心代码Python/ceval-gil.hGIL 的定义和控制算法 在这个过程中:- 求值循环将会获取一个
Code Object并将其转换为一系列的Frame Object - 解释器至少需要一个线程
- 每个线程都有自己的线程状态(Thread State)
Frame Object在被称为帧栈(Frame Stack)的栈中执行- 变量在值栈中被引用
2.6.1 构建线程状态
需要先将帧(Frame)绑定到一个线程上,它才能够被执行。CPython可以在单个解释器中同时运行多个线程。解释器拥有解释器状态(Interpreter State),其中保存了由这些线程状态组成的链表。 CPython需要至少包含一个线程,而每个线程都有它自己的线程状态。 线程状态(PyThreadState)包含了30多个属性,主要包括:
- 线程的唯一标识符;
- 指向其他线程状态的链表;
- 该线程状态由哪一个解释器状态生成;
- 当前正在执行的帧;
- 当前递归的深度;
- 可选的追踪函数;
- 当前正在处理的异常;
- 当前正在处理的任意异步异常;
- 产生多个异常时抛出的异常栈(例如在 except 代码块中触发的异常);
- GIL 计数器;
- 异步生成器计数器。
2.6.2 构建帧对象
编译后的Code Object会被添加到Frame Object中。由于Frame Object是一种Python类型,因此它可以被C或Python引用。执行Code Object中的指令时还需要其他运行时数据,这些数据也包含在Frame Object中,例如局部变量、全局变量和内置模块。
Frame Object的类型是PyObject,它包含了以下属性:
| Attribute | Type | Description |
|---|---|---|
| f_back | PyFrameObject * | 指向栈中前一个帧的指针,如果是第一帧则值为 NULL |
| f_code | PyCodeObject * | 需要执行的 code object |
| f_builtins | PyObject * (dict) | 内置(builtin)模块的符号表 |
| f_globals | PyObject * (dict) | 全局变量的符号表(PyDictObject) |
| f_locals | PyObject * (dict) | 局部变量的符号表 |
| f_valuestack | PyObject ** | 指向最后一个局部变量的指针 |
| f_stacktop | PyObject ** | 指向 f_valuestack 中下一个空闲的插槽(slot) |
| f_trace | PyObject * | 指向自定义追踪函数的指针 |
| f_trace_lines | char | 切换自定义追踪函数在行号级别进行追踪 |
| f_trace_opcodes | char | 切换自定义追踪函数在操作码级别进行追踪 |
| f_gen | Pybject * | 借用的生成器引用或 NULL |
| f_lasti | int | 上一条执行的指令 |
| f_lineno | int | 当前行号 |
| f_iblock | int | 当前帧在 f_blockstack 中的索引 |
| f_executing | char | 标记帧是否仍在执行 |
| f_blockstack | PyTryBlock[] | 保存 for 块,try 块 和 loop 块的序列 |
| f_localsplus | PyObject *[] | 局部变量和栈的联合 |

2.6.3 帧的执行
如前面的编译器和AST部分所述,Code Object除了包含要执行的字节码的二进制编码,还包含了变量列表和符号表。Python中局部和全局变量的值在运行时才会确定,这些值由运行时函数、模块及代码块的调用方式决定。通过函数_PyEval_EvalCode()可以将这些变量的值添加到Frame Object中。除此之外,Frame Object还有一些其他的应用方式,例如协程装饰器会动态的生成一个以目标对象为变量的Frame Object。
PyEval_EvalFrameEx()是一个公共的API,它会调用解释器在eval_frame属性中配置的帧计算函数。基于PEP 523,在Python 3.7实现了帧计算的可插拔性(提供了C API,允许使用第三方代码自定义帧的计算函数)。
_PyEval_EvalFrameDefault()是CPython唯一自带的帧计算默认函数。这个函数是执行帧的关键,它将所有的东西组合到一起,让代码可以运行起来。同时这个函数经历了数十年的持续优化,因为即便是修改一行代码也会对CPython的性能产生巨大的影响。在CPython中,执行任何代码最终都要经过这个帧的求值函数。
2.6.4 值栈
求值循环将从值栈获取输入从而真正的工作起来。
在核心的求值循环中会创建一个值栈。这个栈包含了一系列指向PyObject实例的指针,这些实例可以是变量、函数的引用(在Pythons中也是对象)或其他类型的Python对象。
求值循环中的字节码指令将从值栈中获取输入。
Python/generated_cases.c.h下:
TARGET(BINARY_OP_ADD_INT) { frame->instr_ptr = next_instr; next_instr += 2; INSTRUCTION_STATS(BINARY_OP_ADD_INT); static_assert(INLINE_CACHE_ENTRIES_BINARY_OP == 1, "incorrect cache size"); PyObject *right; PyObject *left; PyObject *res; // _GUARD_BOTH_INT right = stack_pointer[-1]; left = stack_pointer[-2]; { DEOPT_IF(!PyLong_CheckExact(left), BINARY_OP); DEOPT_IF(!PyLong_CheckExact(right), BINARY_OP); } /* Skip 1 cache entry */ // _BINARY_OP_ADD_INT { STAT_INC(BINARY_OP, hit); res = _PyLong_Add((PyLongObject *)left, (PyLongObject *)right); _Py_DECREF_SPECIALIZED(right, (destructor)PyObject_Free); _Py_DECREF_SPECIALIZED(left, (destructor)PyObject_Free); if (res == NULL) goto pop_2_error; } stack_pointer[-2] = res; stack_pointer += -1; DISPATCH(); }比如说:
if left or right: pass编译器会把操作符or编译成BINARY_OR指令:
static intbinop(struct compiler *c, operator_ty op){ switch (op) { case Add: return BINARY_ADD; ... case BitOr: return BINARY_OR; ...在求值循环中,BINARY_OR将从值栈中获取两个值作为左右操作数(left 和 right),随后以这两个对象作为参数调用函数PyNumber_Or():
... case TARGET(BINARY_OR): { PyObject *right = POP(); PyObject *left = TOP(); PyObject *res = PyNumber_Or(left, right); Py_DECREF(left); Py_DECREF(right); SET_TOP(res); if (res == NULL) goto error; DISPATCH(); }最后,将求得的结果res放在栈的顶部,覆写了当前栈顶的值。
每一个操作码都有预定义的栈操作,可以由Python/compile.c中的函数 stack_effect()计算得到。这个函数会返回操作码执行后的值栈中元素数目的增量。
这个增量可能是正值、负值或 0。在执行操作码时,若stack_effect()返回值(例如 +1)与值栈中的增量不匹配,就会抛出一个异常。
举个字节码中使用值栈例子:
import dis
g = 11111
def outer(): c = 22222 def inner(): l = 33333 print(g, c, l) dis.dis(inner)
outer()输出:
0 COPY_FREE_VARS 1
7 2 RESUME 0
8 4 LOAD_CONST 1 (33333) 6 STORE_FAST 0 (l)
9 8 LOAD_GLOBAL 1 (NULL + print) 18 LOAD_GLOBAL 2 (g) 28 LOAD_DEREF 1 (c) 30 LOAD_FAST 0 (l) 32 CALL 3 40 POP_TOP 42 RETURN_CONST 0 (None)18,28,30部分就是将变量加载到值栈当中。
2.6.5 总结
求值循环是编译后的Python代码和底层 C 的扩展模块、标准库及系统调用之间的接口。虽然CPython解释器有一个核心求值循环,但其实你可以同时执行多个循环,不管它们是并发的还是并行的。CPython可以有多个求值循环去同时执行系统上的多个帧。使用帧栈系统让CPython运行在多核或多个 CPU 上。除此之外,CPython的Frame Object API允许帧以异步编程的方式暂停或恢复执行。
2.7 内存管理
使用值栈加载的变量还需要内存分配和管理。要让CPython高效地执行,必须要有可靠的内存管理机制。 由于CPython是基于 C 构建的,它也受到 C 中静态内存分配、自动内存分配和动态内存分配的约束。而 Python 语言的一些特性设计使得我们面临更多的挑战:
- Python是一门动态类型的语言,变量的大小不能在编译阶段得到
- 大多数Python的核心类型大小都是动态的,例如list类型可以是任意长度,dict类型可以有任意数量的键,甚至连int大小也不是固定的,用户不需要指定这些类型的大小
- Python内的变量名可以复用于任意类型 为了解决这些问题,CPython十分依赖动态内存分配,同时借助垃圾回收(GC)和引用计数算法去保证分配的内存可以自动释放。 Python 对象的内存是通过一个统一API自动分配得到的,并不需要Python开发者自己去分配内存。这种设计也意味着CPython的标准库和核心模块都要使用该API去分配内存。 CPython 中使用了两种内存分配器:
- 操作系统层面的内存分配器
malloc,主要用于原始内存作用域; - CPython层面的内存分配器
pymalloc,主要用于PyMem和对象内存作用域。 CPython的内存分配器位于系统的内存分配器之上,并拥有它自己的分配算法。该算法与系统内存分配器类似,不同之处在于它是为CPython定制的:
- 大部分需要分配的内存都是小块且大小固定的内存,因为
PyObject占16字节,PyASCIIObject占42字节,PyCompactUnicodeObject占72字节,PyLongObject占32字节; pymalloc内存分配器最多只能分配256KB大小的内存,更大的内存需要交给系统的内存分配器去处理;pymalloc内存分配器使用GIL而不是系统的线程安全性检查。 内存池算法有以下几种优点:- 这种算法在CPython的主要应用场景(小内存且生命周期较短的对象)下有更好的性能;
- 这种算法使用了GIL而不是系统的线程锁检测;
- 算法使用内存映射mmap()而不是堆上内存分配。
👀关于引用计数的实现
Python对象的基对象定义位于Include/object.h中
/* Nothing is actually declared to be a PyObject, but every pointer to * a Python object can be cast to a PyObject*. This is inheritance built * by hand. Similarly every pointer to a variable-size Python object can, * in addition, be cast to PyVarObject*. */#ifndef Py_GIL_DISABLEDstruct _object {#if (defined(__GNUC__) || defined(__clang__)) \ && !(defined __STDC_VERSION__ && __STDC_VERSION__ >= 201112L) // On C99 and older, anonymous union is a GCC and clang extension __extension__#endif#ifdef _MSC_VER // Ignore MSC warning C4201: "nonstandard extension used: // nameless struct/union" __pragma(warning(push)) __pragma(warning(disable: 4201))#endif union { Py_ssize_t ob_refcnt;#if SIZEOF_VOID_P > 4 PY_UINT32_T ob_refcnt_split[2];#endif };#ifdef _MSC_VER __pragma(warning(pop))#endif
PyTypeObject *ob_type;};#else// Objects that are not owned by any thread use a thread id (tid) of zero.// This includes both immortal objects and objects whose reference count// fields have been merged.#define _Py_UNOWNED_TID 0
// NOTE: In non-free-threaded builds, `struct _PyMutex` is defined in// pycore_lock.h. See pycore_lock.h for more details.struct _PyMutex { uint8_t v; };
struct _object { // ob_tid stores the thread id (or zero). It is also used by the GC and the // trashcan mechanism as a linked list pointer and by the GC to store the // computed "gc_refs" refcount. uintptr_t ob_tid; uint16_t _padding; struct _PyMutex ob_mutex; // per-object lock uint8_t ob_gc_bits; // gc-related state uint32_t ob_ref_local; // local reference count Py_ssize_t ob_ref_shared; // shared (atomic) reference count PyTypeObject *ob_type;};#endif引用计数器ob_refcnt被放在基类之中
static voidpymain_run_python(int *exitcode){ PyObject *main_importer_path = NULL; PyInterpreterState *interp = _PyInterpreterState_GET(); /* pymain_run_stdin() modify the config */ PyConfig *config = (PyConfig*)_PyInterpreterState_GetConfig(interp);
/* ensure path config is written into global variables */ if (_PyStatus_EXCEPTION(_PyPathConfig_UpdateGlobal(config))) { goto error; }
······
// import readline and rlcompleter before script dir is added to sys.path pymain_import_readline(config);
PyObject *path0 = NULL; if (main_importer_path != NULL) { path0 = Py_NewRef(main_importer_path); } else if (!config->safe_path) { int res = _PyPathConfig_ComputeSysPath0(&config->argv, &path0); if (res < 0) { goto error; } else if (res == 0) { Py_CLEAR(path0); } } // XXX Apply config->sys_path_0 in init_interp_main(). We have // to be sure to get readline/rlcompleter imported at the correct time. if (path0 != NULL) { wchar_t *wstr = PyUnicode_AsWideCharString(path0, NULL); if (wstr == NULL) { Py_DECREF(path0); goto error; } config->sys_path_0 = _PyMem_RawWcsdup(wstr); PyMem_Free(wstr); if (config->sys_path_0 == NULL) { Py_DECREF(path0); goto error; } int res = pymain_sys_path_add_path0(interp, path0); Py_DECREF(path0); if (res < 0) { goto error; } }
······
pymain_repl(config, exitcode); goto done;
error: *exitcode = pymain_exit_err_print();
done: _PyInterpreterState_SetNotRunningMain(interp); Py_XDECREF(main_importer_path);}创建新对象时,使用Py_NewRef记录对象的新引用。离开作用域时,使用Py_DECREF释放对象的引用。这两个过程存在于所有的Python C API中。
static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op){ // Non-limited C API and limited C API for Python 3.9 and older access // directly PyObject.ob_refcnt. if (_Py_IsImmortal(op)) { return; } _Py_DECREF_STAT_INC(); if (--op->ob_refcnt == 0) { _Py_Dealloc(op); }}#define Py_DECREF(op) Py_DECREF(_PyObject_CAST(op))