# What CPython is and why anyone would want to study it
CPython 是用 C 编写的 Python 解释器,它是 Python 的实现之一,CPython 的独特之处在于它是最原始、维护最多和最流行的一个。
CPython 实现了 Python,But what is Python?One may simply answer —— Python is a programming language。什么定义了 Python?Python 不像 C 这样的语言,它没有形式规范
CPython 提供了 Python/C API,它允许用 C 扩展 Python 并将 Python 嵌入到 C 中。要有效地使用这个 API,但需要对 CPython 的工作原理有很好的理解
# 理解 CPython 是如何工作的
CPython 被设计为易于维护,能够通过阅读源代码并理解它是做什么的
# Python 程序的执行大致由三个阶段组成:
- 初始化
- 汇编
- 解释
在初始化阶段,CPython 初始化运行 Python 所需的数据结构。它还准备内置类型、配置和加载内置模块、建立导入系统和许多其它事情
接下来是编译阶段,从某种意义上,CPython 是解释器,而不是编译器,因为它不产生机器码。然而,解释器通常在执行源代码之前,将其源代码转换为某种中间表示形式,CPython 也是。这个翻译阶段的工作和典型编译器的工作是一样的:解析源代码并构建 AST(抽象语法树),从 AST 生成字节码,甚至执行一些字节码优化
# 首先字节码是什么?
字节码是一系列指令,每条指令由两个字节组成:一个用于操作码,一个用于参数
def g(x): | |
return x + 3 |
CPython 将 function g 的主体转换为以下字节序列:[124, 0, 100, 1, 23, 0, 83, 0],可以用内置 dis module 来分解它
$ python -m dis example1.py | |
... | |
2 0 LOAD_FAST 0 (x) | |
2 LOAD_CONST 1 (3) | |
4 BINARY_ADD | |
6 RETURN_VALUE |
LOAD_FAST 操作码对应于字节 124,参数为 0。LOAD_CONST 操作码对应字节 100,参数为 1。BINARY_ADD 和 RETURN_VALUE 指令总是分别编码为(23、0)和(83、0),因为它们不需要参数。
CPython 的核心是一个执行字节码的虚拟机,CPython 的 VM 是基于堆栈的。意味着它使用堆栈执行指令来存储和检索数据。LOAD_FAST 指令将局部变量推送到堆栈上。LOAD_CONST 推送一个常量。BINARY_ADD 从堆栈中弹出两个对象,将它们相加并将结果推回,最后,RETURN_VALUE 将弹出堆栈上的任何内容,并将结果返回给调用者。
字节码执行发生在一个巨大的计算循环中,当有指令要执行时,这个循环就会运行。它停下来产生一个值,如果发生错误,它会停止产生值。
当然还有许多问题:
- 参数是什么,还有操作码是什么意思?它们是索引吗?是什么索引?
- VM 是否在堆栈上放置对象的值或引用?
- CPython 怎么知道是局部变量?
- 如果一个参数太大而不能容纳到一个字节中,该怎么办?
- 相加两个数字的的指令是否于连接两个字符串的指令相同?如果是,那么虚拟机如何区分这些操作?
我们需要先了解 CPython VM 的核心概念
# Code objects,Function objects,Frames
# code object
我们看到了一个简单函数的字节码是什么样子,但是实际情况下的 Python 程序是要复杂的多的,VM 如何执行包含函数定义并进行函数调用的模块?
def f(x): | |
return x + 1 | |
print(f(1)) |
它的字节码是什么样子的?首先这个程序定义了函数 f,以 1 作为参数调用 f,并输出调用结果
$ python -m dis example2.py | |
1 0 LOAD_CONST 0 (<code object f at 0x000001AEEC279150, file "work.py", line 1>) | |
2 LOAD_CONST 1 ('f') | |
4 MAKE_FUNCTION 0 | |
6 STORE_NAME 0 (f) | |
5 8 LOAD_NAME 1 (print) | |
10 LOAD_NAME 0 (f) | |
12 LOAD_CONST 2 (1) | |
14 CALL_FUNCTION 1 | |
16 CALL_FUNCTION 1 | |
18 POP_TOP | |
20 LOAD_CONST 3 (None) | |
22 RETURN_VALUE | |
Disassembly of <code object f at 0x000001AEEC279150, file "work.py", line 1>: | |
2 0 LOAD_FAST 0 (x) | |
2 LOAD_CONST 1 (1) | |
4 BINARY_ADD | |
6 RETURN_VALUE |
在第 1 行,我们定义了函数 f,方法是将函数从代码对象(code object)中创建出来,并将名称 f 绑定到该对象上。
作为单个单元(如模块或函数体),执行的代码片段成为代码块。CPython 存储有关代码块在称为代码对象的结构中,它包含字节和类似于块中使用的变量名列表之类的东西。运行模块或调用函数意味着开始计算相应的代码对象。
# function object
然而,函数不仅仅是一个代码对象,它必须包含其它信息,如函数名、docstring、默认参数和在封闭范围中定义的变量值。此信息与代码对象一起存储在函数对象中。MAKE_FUNCTION 指令用于创建它,在 CPython 源代码中函数对象结构的定义之前有一下注释:
函数对象和代码对象不应该混淆:
函数对象是通过 def 语句创建的,它们在其__code__属性中引用一个代码对象,该对象是一个纯语法对象,即仅仅是一些源代码行的编译版本。每个源代码 “fragment” 有一个代码对象,但是每个代码对象可以被零个或多个函数对象引用,这取决于目前为止在源代码中执行 def 语句的次数
几个函数对象怎么会引用一个代码对象呢?
def make_add_x(x): | |
def add_x(y): | |
return x + y | |
return add_x | |
add_4 = make_add_x(4) | |
add_5 = make_add_x(5) |
make_add_x 函数的字节码包含 MAKE_FUNCTION 指令,函数 add_4 () 和 add_5 () 是使用与参数相同的代码对象调用此指令的结果,但有一个不同的参数 x 的值。每个函数都有自己的单元格变量机制,允许我们创建像 add_4 和 add_5 这样的闭包。
先看看代码和函数对象的定义,方便理解它是什么
struct PyCodeObject { | |
PyObject_HEAD | |
int co_argcount; /* #arguments, except *args */ | |
int co_posonlyargcount; /* #positional only arguments */ | |
int co_kwonlyargcount; /* #keyword only arguments */ | |
int co_nlocals; /* #local variables */ | |
int co_stacksize; /* #entries needed for evaluation stack */ | |
int co_flags; /* CO_..., see below */ | |
int co_firstlineno; /* first source line number */ | |
PyObject *co_code; /* instruction opcodes */ | |
PyObject *co_consts; /* list (constants used) */ | |
PyObject *co_names; /* list of strings (names used) */ | |
PyObject *co_varnames; /* tuple of strings (local variable names) */ | |
PyObject *co_freevars; /* tuple of strings (free variable names) */ | |
PyObject *co_cellvars; /* tuple of strings (cell variable names) */ | |
Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */ | |
PyObject *co_filename; /* unicode (where it was loaded from) */ | |
PyObject *co_name; /* unicode (name, for reference) */ | |
/* ... more members ... */ | |
}; |
typedef struct { | |
PyObject_HEAD | |
PyObject *func_code; /* A code object, the __code__ attribute */ | |
PyObject *func_globals; /* A dictionary (other mappings won't do) */ | |
PyObject *func_defaults; /* NULL or a tuple */ | |
PyObject *func_kwdefaults; /* NULL or a dict */ | |
PyObject *func_closure; /* NULL or a tuple of cell objects */ | |
PyObject *func_doc; /* The __doc__ attribute, can be anything */ | |
PyObject *func_name; /* The __name__ attribute, a string object */ | |
PyObject *func_dict; /* The __dict__ attribute, a dict or NULL */ | |
PyObject *func_weakreflist; /* List of weak references */ | |
PyObject *func_module; /* The __module__ attribute, can be anything */ | |
PyObject *func_annotations; /* Annotations, a dict or NULL */ | |
PyObject *func_qualname; /* The qualified name */ | |
vectorcallfunc vectorcall; | |
} PyFunctionObject; |
# frame object
当 VM 执行代码对象时,它必须跟踪变量的值和不断变化的值堆栈。它还需要记住在哪里停止执行当前代码对象以执行另一个对象,以及返回时在哪里继续执行。CPython 将这些信息存储在一个 frameobject 中。frame 提供了代码对象可以执行的状态
struct _frame { | |
PyObject_VAR_HEAD | |
struct _frame *f_back; /* previous frame, or NULL */ | |
PyCodeObject *f_code; /* code segment */ | |
PyObject *f_builtins; /* builtin symbol table (PyDictObject) */ | |
PyObject *f_globals; /* global symbol table (PyDictObject) */ | |
PyObject *f_locals; /* local symbol table (any mapping) */ | |
PyObject **f_valuestack; /* points after the last local */ | |
PyObject **f_stacktop; /* Next free slot in f_valuestack. ... */ | |
PyObject *f_trace; /* Trace function */ | |
char f_trace_lines; /* Emit per-line trace events? */ | |
char f_trace_opcodes; /* Emit per-opcode trace events? */ | |
/* Borrowed reference to a generator, or NULL */ | |
PyObject *f_gen; | |
int f_lasti; /* Last instruction if called */ | |
/* ... */ | |
int f_lineno; /* Current line number */ | |
int f_iblock; /* index in f_blockstack */ | |
char f_executing; /* whether the frame is still executing */ | |
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */ | |
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */ | |
}; |
创建一个 frame 来执行模块的代码对象,每当需要执行另一个代码对象时,CPython 都会创建一个新的 frameobject,每一帧都有对前一帧的引用,因此,帧形成一个帧堆栈,也称为调用堆栈,当前帧位于对于堆栈顶部,当调用一个函数时,一个新的帧被推送到堆栈上。在从当前正在执行的帧返回时,CPython 通过记住其最后处理的指令继续执行前一个帧。在某种意义上,CPython VM 除了构造和执行帧之外,什么也不做
# Threads、Interpreters、Runtime
我们已经看到 3 个重要的概念:
- code object
- function object
- frame object
CPython 还有 3 个:
- a thread state
- an interpreter state
- a runtime state
# thread state
线程状态是一种数据结构,它包含特定于线程的数据,包括调用堆栈、异常状态和调试设置。它不应该于操作系统混淆。使用标准线程模块在单独的线程中运行函数时会发生什么
from threading import Thread | |
def f(): | |
"""Perform an I/O-bound task""" | |
pass | |
t = Thread(target=f) | |
t.start() | |
t.join() |
t.start () 实际上是通过调用 OS 函数(类 UNIX 系统上的 pthread_create () 和 Windows 上的 _startthreadex ( ))来创建一个新的 OS 线程。新创建的线程从负责调用目标的 _thread 模块调用函数。此函数不仅接收目标和目标的参数,而且还接收新操作系统线程中使用的新线程状态。操作系统线程使用自己的线程状态进入求值循环
这里还涉及到一个 GIL(Global Interpreter Lock),它可以防止多个线程同时处于求值循环中。这样做的主要目的是,在不引入更细粒度锁的情况下,保护 CPython 状态不受损坏。Python/C API Reference 清楚的解释了 GIL
Python 解释器不是完全线程安全的,为了支持多线程的 Python 程序,有一个全局锁,称为 GIL,必须由当前线程持有 GIL 才能安全访问 Python 对象。如果没有锁,即使简单的操作也可能在多线程中引起问题:例如当两个线程同时增加同一对象的引用计数时,引用计数最终可能只增加一次,而不是两次
要管理多个线程,需要比线程状态更高级的数据结构
# interpreter and runtime states
实际上,有两种状态:解释器状态和运行时状态
解释器状态是一组线程以及特定于该组的数据。线程共享一些东西,比如加载的模块(sys.module)、内建(builtins)和导入系统(imoprt lib)。
运行时状态是一个全局变量,它存储特定于进程的数据,包括 CPython 的状态(例如,它是否已经初始化?)和 GIL 机制
通常,一个进程的所有线程都属于同一个解释器,但是,有些情况下,可能需要创建一个子解释器来隔离一组线程,mod_wsgi 就是一个例子,它使用不同的解释器来运行 WSGI 应用程序。隔离最明显的效果是,每组线程都有自己版本的所有模块,包括__main__,这是一个全局名称空间
# Architecture summary
CPython 的体系结构,解释器可以看作是一个分层结构
- Runtime:进程的全局状态,包括 GIL 和内存分配机制
- Interpreter:一组线程和它们共享的一些数据,例如导入的模块
- Thread:特定于单个操作系统线程的数据,包括调用堆栈
- Frame:调用堆栈的一个元素,frame 包含一个代码对象并提供执行它的状态
- Evaluation loop:frameobject 执行的位置
简单的概述了 Python 如何执行 Python 程序
- 初始化 CPython
- 将源代码编译为模块的代码对象
- 执行代码对象的字节码
解释器中负责字节码执行的部分成为虚拟机,CPython VM 有几个特别重要的概念:code object、frame object、thread states、intepreter states、runtime,这些数据结构构成了 CPython 体系结构的核心。