2.1 Python解释器工作原理
一、目的
前面的几节我们对环境和库进行了介绍,现在开始正式进入Python代码的学习。首先,我们需要理解Python解释器的工作原理,以更好地体会代码的执行过程。我们已经知道,Python解释器会逐条执行输入的代码,但尚不清楚具体执行机制。了解完整执行流程有助于深入理解Python解释器,而非将其视为黑盒,这对编写代码具有重要意义。
二、工作原理简介
解释器可分为前端和后端两部分:前端包括词法分析器、语法分析器和编译器,后端包含Python虚拟机(PVM)和标准库。前端将Python代码翻译为PVM可执行的字节码,后端的PVM是解释器的核心执行部件,与传统虚拟机不同,它不模拟完整硬件结构,而是作为字节码执行器逐条处理指令。至于为什么不直接让PVM执行Python代码,这是为了提高解释器的灵活性、性能和可维护性。字节码比源码更接近机器指令,虚拟机的执行效率更高。将源码转换为字节码是一个固定且耗时的过程,但只需执行一次,转换为字节码后,后续只需运行字节码,可以避免重复解析。字节码作为平台无关的中间表示,使同一代码能在不同系统的PVM上运行,仅需适配操作系统即可实现跨平台特性。
字节码主要由可执行指令和常量表构成,通常每行代码对应一条或多条指令,代码中的不可变数据存入常量表。Python数据分为运行时可变与不可变类型,为保障安全性和提升性能,编译阶段就将不可变数据嵌入字节码,PVM执行时这些数据已随指令载入内存。可变数据则需运行时动态创建并分配内存,这正是动态语言灵活性的体现。
Python中的数据分为整数、浮点数、字符串、列表、元组、字典等类型,这些统称为对象。代码中对象可通过变量名(字符串符号)标记,即赋值给变量。PVM运行时,变量本质是内存地址(64位系统下为64位数,表示对象起始地址)。传统教程将数据类比为装数字的盒子,这对C语言等编译型语言适用,但在Python中易产生误解。更恰当的比喻是将Python数据视为我们的房产、车辆、工厂等资产,变量则是使用这些资产的钥匙。这些资产通常规模不小,如同Python对象至少占用数十字节。对象内存除存储实际数据外,还包含引用计数、类型指针、预分配空间、跟踪信息等额外开销,这些设计源于Python的动态特性与对象模型。正如房屋不仅放置床铺,车辆也不只具备车轮。此外,我们的资产分为依附于土地的不动产和可随意移动的可动产,Python中的对象也分为不变对象和可变对象。
三、实验
如果读者好奇字节码长什么样,或者什么是词法分析、语法分析、编译,可以进行我们接下来的操作步骤。首先创建一个新虚拟环境,打开Anaconda Prompt,执行下面的命令。
conda create -n compile python=3.9 -y
conda activate compile
pip install graphviz
之后创建一个名为target.py的文件,在文件中写入想要编译的代码,本文的代码如下,这是一个找列表中最大值的程序。
def find_max(numbers):if not numbers:return Nonemax_num = numbers[0]for num in numbers[1:]:if num > max_num:max_num = numreturn max_numif __name__ == '__main__':test_numbers = [3, 1, 4, 1, 5, 9, 2, 6]print(f"数组 {test_numbers} 中的最大值是: {find_max(test_numbers)}")empty_list = []print(f"空数组的最大值是: {find_max(empty_list)}")
最后,我们将下面的代码保存到和target.py同目录的文件中并执行,这是用于生成中间结果的脚本,包括词法分析、语法分析和编译的结果,分析结果会保存到同目录下的txt文件中,另外,语法分析生成的抽象语法树会保存到一个pdf文件中。
import dis
import ast
import inspect
import tokenize
import target
from io import BytesIO
from graphviz import Digraphdef plot_ast(node, graph=None, parent=None, edge_label=""):if graph is None:graph = Digraph()node_name = str(id(node))label = type(node).__name__if hasattr(node, 'ctx'):label = f"{type(node).__name__}(id='{getattr(node, 'id', '')}')" if hasattr(node, 'id') else type(node).__name__graph.node(node_name, label=label)if parent:graph.edge(parent, node_name, label=edge_label)for field, value in ast.iter_fields(node):if field == 'ctx':continueif isinstance(value, ast.AST):plot_ast(value, graph, node_name, field)elif isinstance(value, list):for item in value:if isinstance(item, ast.AST):plot_ast(item, graph, node_name, field)return graphdef analysis():save_path = 'analysis_result.txt'file = open(save_path, 'w', encoding='utf-8')code = inspect.getsource(target)code_bytes = code.encode('utf-8')tokens = tokenize.tokenize(BytesIO(code_bytes).readline)print('词法分析结果:', file=file)for token in tokens:print(f"\tType: {tokenize.tok_name[token.type]}, "f"Value: {token.string!r}, "f"Position: {token.start}", file=file)print("\n\n语法分析结果:", file=file)tree = ast.parse(code)print(ast.dump(tree, indent=2), file=file)graph = plot_ast(tree)graph.render("ast_tree", view=True)code_obj = compile(tree, filename="<ast>", mode="exec")print("\n\n反汇编后的字节码:", file=file)dis.dis(code_obj, file=file)print("实际字节码(十六进制):", code_obj.co_code.hex(), file=file)print("常量表:", code_obj.co_consts, file=file)if __name__ == '__main__':analysis()