Python 程序从你敲下回车到看到结果,背后并不是魔法,而是一条清晰的流水线。理解这个流程,能帮你解释很多现象:为什么脚本不需要编译?为什么某些语法错在运行前就能被发现?为什么 dis 模块能窥探代码的性能细节?
整个流程分为五个核心阶段:
1. 源码(Source Code)
就是你写的 .py 文件内容,一串 UTF-8 文本。CPython 会先检查编码声明,然后读取全部内容。
2. 词法分析(Lexical Analysis)
做什么:把字符串切成一个个有意义的“单词”,称为 Token。比如 a = 3 + 2 会被切成 NAME 'a'、EQUALS、NUMBER 3、PLUS、NUMBER 2。
实用工具:tokenize 模块可以直观看到这一步的输出。
import tokenize
import io
code = "x = 10 + sum([1,2])"
for tok in tokenize.generate_tokens(io.StringIO(code).readline):
print(tok)
作用:这个阶段只做简单的切割和分类,不关心结构是否正确。如果出现非法字符(如中文全角括号),会在这一步报错。
3. 语法分析(Parsing)→ 生成 AST
做什么:把 Token 流按 Python 的语法规则组装成一棵抽象语法树(AST)。这棵树用对象表示代码的结构,比如 a = 1 + 2 的 AST 会有一个赋值节点,右边是一个加法节点,下面挂着两个数值节点。
实用工具:ast 模块。
import ast
tree = ast.parse("x = 10 + sum([1,2])")
print(ast.dump(tree, indent=2))
作用:这个阶段会检查语法是否正确(比如括号是否匹配,if 后面有没有冒号)。如果语法错误,你会在运行前就看到 SyntaxError。AST 也是代码分析、Linter、代码格式化工具(如 black)的底层基础。
4. 编译成字节码(Bytecode Compilation)
做什么:把 AST 转换成一种叫字节码的低级指令序列。这些指令是与平台无关的,类似汇编代码,但操作的不是硬件寄存器,而是 Python 虚拟机的运行时栈和变量表。
实用工具:py_compile 模块可以生成 .pyc 文件;dis 模块可以把字节码反汇编成人类可读的样式。
import dis
def add(a, b):
return a + b
dis.dis(add)
输出类似:
2 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
作用:字节码会被缓存到 pycache 目录下的 .pyc 文件中。下次运行脚本时,如果源码没变,解释器直接加载字节码,跳过词法、语法分析,加快启动速度。字节码的指令也是性能分析的重要入口(比如用 dis 看为什么一行代码执行得更慢)。
5. 虚拟机执行(Execution by Python Virtual Machine)
做什么:CPython 的“心脏”是一个基于栈的虚拟机。它逐条读取字节码指令,操作运行时栈和局部变量,最终完成函数调用、对象创建、算术运算等动作。
关键细节:
- 每个代码块(模块、函数、类)都会生成一个代码对象(
code object),里面存着字节码、常量、变量名等信息。 - 解释器为每次函数调用创建一个栈帧,用来存放该函数的局部变量、操作数栈和当前执行位置。栈帧串联成调用链。
- 虚拟机在运行时完全控制对象的创建和销毁,Python 的内存分配、引用计数、垃圾回收都在这一层处理。
实际可见的体现:当你执行一个 .py 文件时,可以认为 CPython 帮你把上述所有步骤一口气跑完了。如果你在 import 一个模块时修改了源码,解释器会重新生成字节码,无需手动编译。
整个流程的实用意义
- 为什么 Python 是解释型的:因为它没有生成独立的可执行文件,而是靠虚拟机读字节码执行。不过严格来说,内部还是有编译步骤(源码→字节码)。
- 为什么首次运行比后续慢:因为要经历完整的词法、语法分析和字节码编译,之后加载
.pyc就快得多。 - 为什么可以在运行时动态修改代码:因为
eval和exec函数可以独立启动这整条流水线,动态执行字符串。 - 为什么调试和性能分析工具能工作:因为可以拦截字节码、钩入虚拟机执行过程(比如
sys.settrace做调试,cProfile统计时间)。
理解这条流水线,你就不再只是“会用 Python”,而是开始看到幕布背后的运作机制,对排查疑难问题、理解性能瓶颈都有直接帮助。