春暖花开

CPython 学习笔记

本文为查阅相关资料的过程中随手记录的,故内容较为杂乱,无任何章节可言。

Python 源代码执行流程:

Python 基于栈式执行,CPython 主要使用三种类型的栈:

  • call stack:一个 frame 表示一个活动的函数调用
  • data stack:也称作 evaluation stack,每一个函数对应一个 data stack,函数在其中进行数据操纵,包括函数参数,返回地址等
  • block stack:用于跟踪控制结构的确定类型:如循环,try/except 块,with 块等

反汇编Python字节码:dis 库,例如:

1
2
3
4
5
6
import dis
def hello():
print('hello, world!')
dis.dis(hello)

输出如下:

1
2
3
4
5
6
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('hello')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE

可通过 __code__ 属性访问函数 code object,如 hello.__code__,同时,code 对象拥有如下一些属性:

  • co_consts:由函数中出现的任何文字(如常数、字符串)组成的元组
  • co_varnames:一个包含所有函数体中使用到的局部变量的名字的元组
  • co_names:一个包含任何函数中引用过的非局部名字的元组,包括变量名,调用的函数名等
  • co_code:原始字节码

Python 解释器是一个字节码解释器,模拟真实计算机的软件。

解释器在执行字节码时操作的是数据栈。

Frame:一个 frame 是一些信息的集合和代码的上下文。frame 在 Python 代码执行时动态地创建和销毁,每个 frame 对应函数的一次调用,所以每个 frame 只有一个 code object 与之关联,相反,一个 code object 可以有多个 frame,如递归函数。总的来说,Python 程序的每个作用域有一个 frame,比如每个模块,每个函数调用,每个类定义。

每个 frame 都有自己特定的 data stack 和 block stack 。

例如:

1
2
3
4
5
6
7
8
9
10
11
>>> def bar(y):
... z = y + 3 # <--- (3) ... and the interpreter is here.
... return z
...
>>> def foo():
... a = 1
... b = 2
... return a + bar(b) # <--- (2) ... which is returning a call to bar ...
...
>>> foo() # <--- (1) We're in the middle of a call to foo ...
3

假设此时,解释器正在 foo 函数的调用中,紧接着调用 bar ,则 call stack,data stack,block stack 的示意图如下:

现在,解释器在 bar 函数的调用中,call stack 中有三个 frame,一旦 bar 返回,与之对应的 frame 就会被弹出并丢弃。

字节码指令RETURN_VALUE告诉解释器在 frame 间传递一个值。首先,它把位于调用栈栈顶的 frame 中的 data stack 的栈顶值弹出。然后把整个 frame 弹出丢弃,最后把这个值压到下一个 frame 的 data stack 中。

Python 代码编译过程:

  • 解析源代码生成解析树(parse tree)
  • 将解析树转换成抽象语法树 AST
  • 生成符号表
  • 根据 AST 生成 code object:AST -> 控制流图 -> code object

使用 parser 模块可以获得 Python 代码的 parse tree:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
>>>code_str = """def hello_world():
return 'hello world'
"""
>>> import parser
>>> from pprint import pprint
>>> st = parser.suite(code_str)
>>> pprint(parser.st2list(st))
[257,
[269,
[294,
[263,
[1, 'def'],
[1, 'hello_world'],
[264, [7, '('], [8, ')']],
[11, ':'],
[303,
[4, ''],
[5, ''],
[269,
[270,
[271,
[277,
[280,
[1, 'return'],
[330,
[304,
[308,
[309,
[310,
[311,
[314,
[315,
[316,
[317,
[318,
[319,
[320,
[321,
[322, [323, [3, '"hello world"']]]]]]]]]]]]]]]]]]]],
[4, '']]],
[6, '']]]]],
[4, ''],
[0, '']]
>>>

使用 ast 模块来获取 Python 代码的 AST:

1
2
3
4
5
6
7
8
9
10
11
>>>code_str = """def hello_world():
return 'hello world'
"""
>>> import ast
>>> import pprint
>>> node = ast.parse(code_str, mode="exec")
>>> ast.dump(node)
("Module(body=[FunctionDef(name='hello_world', args=arguments(args=[], "
'vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), '
"body=[Return(value=Str(s='hello world'))], decorator_list=[], "
'returns=None)])')

Names and binding in Python:在 Python 中,对象通过名字引用,binding 即名字与对象的绑定,名字仅仅是与对象相联系的符号,是对象的引用,没有类型,对象具有类型。

Code Blocks: A code block is a piece of program code that is executed as a single unit in python. Modules, functions and classes are all examples of code blocks. Commands typed in interactively at the REPL, script commands run with the -c option are also code blocks.

Namespaces:在 Python 中,名字空间通过字典映射实现,包含所有内建函数的内建名字空间可通过 __builtins__.__dict__ 访问。

Python Objects

Python 中的所有对象,函数、变量等,都是 PyObject ,或者说 PyObject 是所有 Python 对象的超类。

PyObject 定义:

1
2
3
4
5
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;

_PyObject_HEAD_EXTRA 是一个宏,ob_refcnt 用于内存管理,表明该对象的引用次数,ob_type 是指向 type object 的指针。一个有趣的事,type 函数的 type 就是 type,因此,type 函数的 ob_type 域指向自身。

  • type 类型是所有内建类型的元类型
  • object 类型是所有用户自定义类型的元类型

Code Objects

Code Objects 是已编译可执行 Python 代码,其中包含可运行的字节码指令。

给定一个函数,我们可以通过 __code__ 属性访问函数体的 code object:

1
2
3
4
5
>>> def hello():
... print("hello, world!")
...
>>> hello.__code__
<code object hello at 0x7fd58d3b4ed0, file "<stdin>", line 1>

Code Object 的 co_code 属性包含字节码指令序列:

1
2
>>> hello.__code__.co_code
b't\x00d\x01\x83\x01\x01\x00d\x00S\x00'

字节码指令的大小为两字节,一字节为 opcode(现为两字节大小),另一字节为 opcode 的参数。如果 opcode 不需要参数,则对应参数部分为 0。

Frames Objects

Frame object 为 code object 的执行提供上下文信息。

通过 sys._getframe() 访问 frame object:

1
2
3
4
5
6
>>> import sys
>>> f = sys._getframe()
>>> f
<frame object at 0x7fd580fd0b28>
>>> dir(f)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'f_back', 'f_builtins', 'f_code', 'f_globals', 'f_lasti', 'f_lineno', 'f_locals', 'f_trace']

在一个 code object 执行前,都需要创建一个 frame object,该 frame object 包含该 code object 执行需要的所有名字空间,一个指向当前执行的线程的引用,data stack,block stack,以及其它 code object 执行需要的重要信息。

在 Python 代码执行过程中,需要通过调用 PyFrame_New 函数创建很多次 frame object,为了减少函数调用带来的开销,采用了两种优化方式:

  • Code object 有一个域 co_zombieframe ,当一个 object 执行过后,对应的 frame 不会立刻释放,而会保存到 co_zombieframe 中,从而当下次执行相同的 code object 时,不需要再次分配内存创建新的 frame;
  • Python 虚拟机维护一个预分配好的空闲 frame 列表,供 code object 使用;

解释器状态和线程状态

Interpreter state 保存一个 Python 进程中一系列协作线程共享的全局状态。

一个 thread state 与一个正在运行的 Python 进程的 native OS thread 相联系。

解释器状态、线程状态和 frame 之间的关系:

The evaluation loop, ceval.c

Python 虚拟机循环迭代 code object 的每一条指令并执行:Python/ceval.c,关键函数:PyEval_EvalFrameEx

PyEval_EvalFrameEx 函数执行 Python 字节码之前,需要进行大量的设置工作,包括错误检查、frame 创建和初始化等(_PyEval_EvalCodeWithName 函数)。

Block Stack

通过 block stack,能够很方便地实现异常处理,且仅用于处理循环和异常。PyFrame_BlockSetup 函数创建一个一个新的 block 并 push 到 block stack。

从 Class code 到字节码

创建类的过程大致如下:

  • 将类定义语句放入一个 code object 中
  • 确定该类实例的合适元类
  • 为该类的名字空间准备一个字典
  • 在该名字空间内执行上述对应的 code object
  • 通过实例化元类创建类对象

元类确定:

  • 如果没有给定基类或明确的元类,则使用 type()
  • 如果给定了明确的元类但不是 type() 的实例,则使用该元类
  • 如果给定了 type() 的实例作为明确元类,或者定义了基类,则使用最派生的元类。

Generator

生成器对象包含一个 frame object 和 code object,当生成器函数被调用时,它并不会运行,而是返回一个生成器对象,只有将生成器对象作为一个参数传入内建的 next 函数时,生成器对象才能够运行:即在生成器对象所包含的 frame object 内调用 PyEval_EvalFrameEx 函数。

当生成器函数执行遇到 YIELD_VALUE opcode 时,该 opcode 会使得执行暂停,并将栈顶值返回给调用者。暂停意味着当前正在执行的 frame 退出了,但是它并不会被释放,因为该 frame 还在被生成器对象引用,并且能够再次执行,即再次以该 frame 作为参数调用 PyEval_EvalFrameEx 函数时。

References

0%