Python 代码究竟是怎样“跑起来”的?从源码、AST、字节码到 CPython 专门化解释器
很多人第一次学习 Python 时,会形成一个很自然的印象:
Python 是解释型语言,所以解释器大概是一行一行读取源码,然后一行一行执行。
这个理解不能说完全错,但如果你继续深入 Python 编程、性能优化、框架原理甚至 CPython 源码,就会发现真实过程要有意思得多。
看看这个再简单不过的函数:
def add(a, b):
return a + b
当我们执行:
add(1, 2)
CPU 并不是直接理解 return a + b。
在现代 CPython 中,它大致要经历这样一条链路:
Python 源代码
↓
Tokenize / Parse
↓
AST(抽象语法树)
↓
符号表分析
↓
Instruction Sequence
↓
CFG(控制流图)与编译期优化
↓
Bytecode
↓
Code Object
↓
Function Object
↓
Frame
↓
CPython Interpreter
↓
Python Object / C API
↓
CPU
CPython 官方内部文档给出的编译流程也基本如此:源码首先被词法分析、解析为 AST,然后生成指令序列,构造并优化控制流图,最终组装成 bytecode 和 PyCodeObject。(GitHub)
而更值得关注的是:从 Python 3.11 开始,CPython 执行的字节码甚至可能在运行过程中根据真实数据类型被“专门化”。
这也是为什么同一段 Python 代码,在运行一段时间以后,有时会比刚启动时更快。(Python Enhancement Proposals (PEPs))
本文就沿着 add() 这个函数,一层一层把 Python 的执行过程拆开。
**版本说明:**本文讨论的是 CPython,重点面向 Python 3.11 之后的实现。下面部分
dis输出使用 CPython 3.13.x 作为示例。Python 官方明确说明 bytecode 属于 CPython 实现细节,不保证不同 Python 版本之间保持不变,因此你在 3.12、3.13、3.14 中看到的操作码名称可能不同。(Python documentation)
一、第一站:源码并不会直接交给 CPU
我们的源码是:
def add(a, b):
return a + b
对人来说,它已经足够清楚:
- 定义一个函数;
- 接受两个参数;
- 对二者执行加法;
- 返回结果。
但是解释器首先需要回答一些更基础的问题:
def是什么?add是函数名还是变量?a、b属于什么作用域?+是哪一种表达式?return的返回值是什么?- 整个程序的语法是否合法?
所以源码首先要被解析。
我们可以自己看看 Python 眼中的这段代码。
import ast
source = """
def add(a, b):
return a + b
"""
tree = ast.parse(source)
print(
ast.dump(
tree,
indent=4
)
)
你会看到类似:
Module(
body=[
FunctionDef(
name='add',
args=arguments(
args=[
arg(arg='a'),
arg(arg='b')
]
),
body=[
Return(
value=BinOp(
left=Name(id='a', ctx=Load()),
op=Add(),
right=Name(id='b', ctx=Load())
)
)
]
)
]
)
这就是 AST:Abstract Syntax Tree,抽象语法树。
它已经不太关心源码究竟写成:
return a+b
还是:
return a + b
而更关心程序的结构:
FunctionDef: add
│
├── 参数 a
├── 参数 b
│
└── Return
│
└── BinOp (+)
├── Name(a)
└── Name(b)
换句话说:
源码描述“程序长什么样”,AST 描述“程序是什么意思”。
CPython 当前使用 PEG parser,将 token 流解析为 AST;AST 随后进入编译器的后续阶段。(GitHub)
这也是为什么 ast 模块在静态分析、代码检查、自动重构、代码生成等工具中如此重要。
例如,你完全可以找出某段代码里所有函数:
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
print(node.name)
输出:
add
很多 lint、IDE、代码扫描器的思想,本质上都和这种结构化分析有关。
二、AST 之后发生什么?进入真正的“编译阶段”
这里有一个非常重要的认知:
CPython 并不是“不编译”。
它只是不是通常意义上直接把 Python 源码编译成 x86、ARM 机器码。
通常情况下,CPython 会把源码编译成自己的 bytecode(字节码)。
根据 CPython 编译器内部文档,大致经历:
Source
↓
AST
↓
Symbol Table
↓
Instruction Sequence
↓
CFG
↓
Optimization
↓
Bytecode
↓
Code Object
其中还有一个经常被 Python 开发者忽视的重要阶段:符号表分析。
例如:
x = 10
def foo():
y = 20
return x + y
编译器必须判断:
x → global
y → local
再复杂一点:
def outer():
x = 10
def inner():
return x
return inner
这里 x 又会涉及 closure / free variable。
这些信息不能等真正执行时才完全从零开始判断,编译阶段就会提前完成大量作用域分析。
三、Code Object:真正值得认识的 Python 内部对象
我们现在直接拿到 add 的 code object:
def add(a, b):
return a + b
code = add.__code__
print(code)
可能得到:
<code object add at 0x..., file "...", line 1>
可以进一步观察:
print(code.co_name)
print(code.co_argcount)
print(code.co_varnames)
print(code.co_consts)
print(code.co_stacksize)
例如:
add
2
('a', 'b')
(None,)
2
Code Object 可以理解为:
已经编译完成、等待执行的一份 Python 程序描述。
其中包含的不只是 bytecode,还包括:
co_code 字节码相关信息
co_consts 常量
co_names 使用到的名称
co_varnames 局部变量
co_argcount 参数数量
co_stacksize 所需执行栈大小
co_filename 来源文件
...
Python 官方数据模型同样将 code object 定义为表示已经 byte-compile 的可执行 Python 代码;函数对象和 code object 并不是同一个东西。函数还拥有 globals、默认参数等运行时上下文。(Python documentation)
这一点非常重要。
四、一个容易被忽略的细节:def 本身也是要执行的
很多人以为:
def add(a, b):
return a + b
运行模块时,解释器直接“得到”了函数。
其实还不是。
我们自己编译一下:
source = """
def add(a, b):
return a + b
"""
module_code = compile(
source,
"demo.py",
"exec"
)
print(module_code.co_consts)
你可能看到:
(
<code object add at ...>,
None
)
有趣的地方来了。
add 的 code object 被放进了模块 code object 的常量区域。
继续:
import dis
dis.dis(module_code)
在某些现代 CPython 版本中,可以看到类似:
LOAD_CONST <code object add ...>
MAKE_FUNCTION
STORE_NAME add
也就是说,执行:
def add(...):
实际上可以粗略理解为:
取得 add 的 Code Object
↓
创建 Function Object
↓
把函数对象绑定给名字 add
所以:
add
是一个 function object,
而:
add.__code__
才是它背后的 code object。
这个区别在理解:
- 装饰器;
- 闭包;
- 动态函数;
- monkey patch;
- descriptor;
- 函数热替换;
时非常关键。
五、dis.dis(add) 到底能看到什么?
终于来到很多 Python 进阶教程中最神秘的工具:
import dis
dis.dis(add)
以某些 CPython 3.13 构建为例,可能出现:
RESUME
LOAD_FAST_LOAD_FAST (a, b)
BINARY_OP (+)
RETURN_VALUE
不同版本输出会不同,所以不要死记操作码。
Python 官方专门提醒:bytecode 是 CPython implementation detail,操作码可以随版本增加、删除或者修改。(Python documentation)
真正应该学习的是怎么读。
1. LOAD_FAST
假设版本中显示:
LOAD_FAST 0 (a)
LOAD_FAST 1 (b)
含义可以粗略理解为:
把局部变量 a 放到执行栈
把局部变量 b 放到执行栈
为什么叫 FAST?
因为局部变量在函数执行时拥有专门的高效存储结构,并不是每次都简单等价于:
locals()["a"]
进行字典查询。
2. BINARY_OP
接下来:
BINARY_OP 0 (+)
意味着从栈顶取出两个对象:
┌─────┐
TOP → │ b │
├─────┤
│ a │
└─────┘
执行:
a + b
再把结果压回去:
┌───────┐
TOP → │ a + b │
└───────┘
官方 dis 文档对 BINARY_OP 的抽象描述基本就是:
rhs = STACK.pop()
lhs = STACK.pop()
STACK.append(lhs op rhs)
3. RETURN_VALUE
最后:
RETURN_VALUE
把当前栈顶结果作为函数返回值。
所以:
def add(a, b):
return a + b
从虚拟机角度看,可以非常粗略地理解成:
LOAD a
LOAD b
ADD
RETURN
这已经非常接近虚拟机真正关心的世界了。
六、Bytecode 和 Code Object 到底是什么关系?
可以用一个简单比喻。
假设 Code Object 是一本“施工手册”:
Code Object
├── bytecode:施工步骤
├── constants:材料清单
├── variable names:变量目录
├── source positions:源码位置
├── stack size:需要多大的工作台
└── exception info:异常处理路径
那么 bytecode 只是其中最核心的一部分,并不等于整个 Code Object。
实际上 Python 官方 C API 文档甚至特别提醒:现代 CPython 中,通过某些接口取得的 co_code 表示并不一定就是解释器最终实际执行的内部字节码形态,因为运行时还可能进行专门化。(Python documentation)
这就引出了本文最精彩的一部分。
七、CPython Interpreter 怎样执行 Bytecode?
调用:
add(1, 2)
时,解释器还需要一个东西:
Frame
Code Object 更像“静态程序”。
Frame 则保存“一次具体执行”的动态状态。
可以粗略理解:
Code Object
+
实际参数
+
globals
+
builtins
+
指令位置
+
执行栈
+
异常状态
↓
Frame
如果:
add(1, 2)
add(100, 200)
它们执行的是同一个 add.__code__,
但属于两次不同的函数调用,因此会拥有不同的执行状态。
CPython 内部解释器文档描述得很明确:执行一个 CodeObject 时会构造 frame,frame 保存指令指针、globals、builtins 等动态执行状态,随后交给 evaluation loop 执行。(GitHub)
经典理解中,解释器核心逻辑可以想象成:
for (;;) {
opcode = NEXT_OPCODE();
switch (opcode) {
case LOAD_FAST:
...
break;
case BINARY_OP:
...
break;
case RETURN_VALUE:
...
break;
}
}
当然,真实 CPython 要复杂得多。
但这个模型非常有用:
CPython interpreter 本质上是一台执行 Python bytecode 的虚拟机。
八、问题来了:a + b 到底慢在哪里?
假设我们第一次执行:
add(1, 2)
人类已经知道:
a 是 int
b 是 int
但函数定义本身并不知道。
下一次可能是:
add("Hello ", "Python")
甚至:
class Money:
def __add__(self, other):
...
add(Money(), Money())
所以一个通用的:
BINARY_OP +
不能简单等同于 CPU 的整数加法。
它需要考虑:
int + int?
float + float?
str + str?
用户自定义 __add__?
右操作数 __radd__?
子类?
失败后抛 TypeError?
动态语言的灵活性,是需要运行时成本的。
那么问题来了:
如果一个
BINARY_OP连续执行了成千上万次,而且每一次都是int + int,为什么还要永远走最通用的路径?
这正是 specializing adaptive interpreter 要解决的问题。
九、Adaptive / Specializing Interpreter 到底优化什么?
从 Python 3.11 开始,CPython 引入了 specializing adaptive interpreter。
它的核心思想其实非常朴素:
先观察,再下注;下注正确就走快路,环境变化就退回来。
PEP 659 将这个过程称为 specialization 和 quickening:一个适合优化的指令运行足够多次以后,可以被替换成针对实际类型或值模式优化的版本;如果之后假设持续失败,还可以反向 de-optimize。(Python Enhancement Proposals (PEPs))
例如:
def add(a, b):
return a + b
刚开始可能是:
BINARY_OP
假设不断执行:
for _ in range(100_000):
add(1, 2)
解释器逐渐观察到:
这里几乎一直都是:
int + int
int + int
int + int
int + int
...
那么通用指令就可能专门化成类似:
BINARY_OP_ADD_INT
在一个 CPython 3.13.x 环境中,预热后实际就可能观察到:
BINARY_OP_ADD_INT
这背后的意思不是:
“Python 突然变成 C 了。”
而是:
“解释器已经知道这个热点位置大概率处理两个整数,因此可以绕开一部分通用动态分派工作。”
PEP 659 明确指出,这种 specialization 即使没有把代码生成成本地机器码,同样可以带来性能收益。(Python Enhancement Proposals (PEPs))
因此它和传统意义上的 JIT 不是同一个概念。
十、亲手观察:“预热前”和“预热后”有什么不同?
这是本文最推荐你亲自运行的实验。
import dis
def add(a, b):
return a + b
print("=== before ===")
dis.dis(
add,
adaptive=True,
show_caches=True
)
for _ in range(100_000):
add(1, 2)
print("=== after ===")
dis.dis(
add,
adaptive=True,
show_caches=True
)
在支持 specialization 的 CPython 中,你可能观察到类似变化:
预热前
BINARY_OP
CACHE
预热后
BINARY_OP_ADD_INT
CACHE
注意:
dis.dis(add)
和:
dis.dis(
add,
adaptive=True,
show_caches=True
)
观察到的东西并不完全一样。
官方 dis 文档说明:
show_caches=True可以显示 specialization 使用的 inline cache;adaptive=True可以显示运行时已经专门化的 bytecode,而不是只看原始形式。(Python documentation)
在 Python 3.14 的 python -m dis 命令行工具中,还增加了 -S / --specialized 来显示 specialized bytecode。(Python documentation)
十一、属性访问的优化甚至更有意思
看一个更接近真实业务代码的例子:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def total(p):
return p.x + p.y
先看:
p = Point(10, 20)
dis.dis(
total,
adaptive=True,
show_caches=True
)
然后预热:
for _ in range(100_000):
total(p)
再次:
dis.dis(
total,
adaptive=True,
show_caches=True
)
在合适的 CPython 版本中,你可能看到类似:
LOAD_ATTR
逐渐变成:
LOAD_ATTR_INSTANCE_VALUE
以及:
BINARY_OP
变成:
BINARY_OP_ADD_INT
为什么 LOAD_ATTR 很适合优化?
因为一般意义上的:
obj.x
背后可能涉及:
实例属性?
类属性?
descriptor?
property?
__getattribute__?
__getattr__?
__slots__?
module attribute?
这是一个非常动态的操作。
但是如果运行时不断证明:
这里一直是普通实例
x 一直位于稳定的位置
类结构没有变化
解释器就可以缓存一些信息,让下一次属性访问走更短的路径。
PEP 659 甚至专门以 LOAD_ATTR、LOAD_GLOBAL 为案例说明了这种 specialization:针对实例属性、模块属性、__slots__ 等不同情况使用更加专门的执行路径,并配合 inline cache 保存辅助信息。(Python Enhancement Proposals (PEPs))
这就是现代 CPython 性能优化越来越重要的方向之一:
不是取消 Python 的动态能力,而是让“稳定的动态行为”更便宜。
十二、Inline Cache 又是什么?
你使用:
dis.dis(
total,
adaptive=True,
show_caches=True
)
可能看到不少:
CACHE
CACHE
CACHE
不要把它们简单理解成普通 Python 指令。
它们主要用于保存 specialization 所需要的运行时信息。
可以粗略想象:
LOAD_ATTR x
│
├── 这个类型是否还是原来的类型?
├── 字典结构有没有变化?
├── 属性大致在哪里?
└── specialization 是否持续成功?
下一次再执行时,如果这些假设仍然成立,就走快路。
如果不成立:
假设失败
↓
fallback
↓
通用执行路径
↓
必要时重新 specialization
所以它为什么叫 adaptive?
因为它不是:
优化一次 → 永远不变
而更接近:
观察
↓
专门化
↓
继续验证
├── 成功 → 保持优化
└── 失败 → 降级 / 重新适应
PEP 659 描述的 specialized instruction 会根据成功和失败调整计数,在假设长期失效时重新退回 adaptive 状态。(Python Enhancement Proposals (PEPs))
十三、终于回答:为什么同一段 Python 代码运行一段时间后可能变快?
现在可以给出一个完整答案。
原因一:解释器完成了 specialization
刚执行时:
BINARY_OP
LOAD_ATTR
LOAD_GLOBAL
CALL
可能还需要走较通用的逻辑。
热点代码执行一段时间以后,解释器已经掌握真实运行模式,例如:
这里一直是 int + int
这里一直访问同一种实例
这个 global 查找结构一直稳定
这里一直调用相似目标
于是部分操作可以进入 specialized fast path。(Python Enhancement Proposals (PEPs))
原因二:Inline Cache 已经“热起来”
第一次执行时,缓存还没有足够的信息。
后来:
类型信息
属性结构
命名空间状态
热点执行情况
等信息逐渐稳定,重复操作就可能减少一些重复工作。
原因三:还有 CPU 和系统层面的“热身”
不要把所有 warm-up 都归功于 Python specialization。
程序执行几轮后,性能改善还可能来自:
- CPU instruction cache;
- CPU data cache;
- branch predictor;
- 操作系统文件缓存;
- 动态链接库初始化;
- 第三方库自己的缓存;
- 内存分配器状态;
- 网络连接池;
- 数据库连接池。
因此:
“第二次比第一次快”不等于“CPython 一定进行了 specialization”。
要判断 specialization,最直接的方法仍然是观察:
dis.dis(
func,
adaptive=True,
show_caches=True
)
十四、这对性能测试有什么实际影响?
影响非常大。
下面这种测试:
import time
start = time.perf_counter()
result = add(1, 2)
end = time.perf_counter()
print(end - start)
几乎没有什么性能分析价值。
你测到的可能包括:
函数首次执行
+
解释器状态
+
计时器本身开销
+
CPU 调度
+
缓存冷启动
更合理的微基准测试方式是使用:
import timeit
def add(a, b):
return a + b
for _ in range(50_000):
add(1, 2)
result = timeit.repeat(
"add(1, 2)",
globals=globals(),
number=1_000_000,
repeat=5
)
print(result)
print("best:", min(result))
这里有三个实践原则。
第一,不要只执行一次。
单次测试噪声极大。
第二,要区分 cold performance 和 warm performance。
服务启动性能与长期运行性能并不是一个问题。
第三,不要为了“帮助 specialization”写奇怪代码。
优化顺序仍然应该是:
先选择正确算法
↓
减少无意义工作
↓
减少 I/O
↓
改善数据结构
↓
profile 找热点
↓
最后研究解释器级优化
把 O(n²) 改成 O(n log n),通常比研究某个 bytecode 快几个百分点重要得多。
十五、.pyc 又位于哪一层?
当我们导入 Python 模块时,经常能看到:
__pycache__/
里面有:
xxx.cpython-314.pyc
一个非常常见的误解是:
.pyc是不是类似 C 编译出来的机器码?
不是。
它与 CPython bytecode / code object 层更接近,而不是 CPU 可以直接执行的原生机器指令。
当模块再次 import 时,如果缓存仍然有效,Python 可以利用缓存结果,从而避免重新完成全部源码编译流程。Python import 系统在加载 .pyc 前还会检查缓存是否与源码匹配。(Python documentation)
因此可以粗略理解:
第一次:
.py
↓
parse
↓
compile
↓
code object / bytecode
↓
可能生成 .pyc
后续 import:
.pyc
↓
恢复已编译结果
↓
执行
但注意:
.pyc主要减少的是编译相关工作,并不会让你函数中的每个a + b自动变成本地机器指令。
十六、一套非常实用的 Python“拆解程序”工具箱
以后遇到一个陌生函数,可以按下面顺序研究。
假设:
def calculate(price, count):
total = price * count
return total * 0.9
第一步:看 AST
import ast
import inspect
source = inspect.getsource(calculate)
tree = ast.parse(source)
print(ast.dump(tree, indent=4))
解决:
“编译器怎么看这段源码?”
第二步:看 Code Object
code = calculate.__code__
print(code.co_varnames)
print(code.co_consts)
print(code.co_names)
print(code.co_stacksize)
解决:
“这段函数编译后保存了哪些静态信息?”
第三步:看普通 bytecode
import dis
dis.dis(calculate)
解决:
“源码最终被拆成了哪些虚拟机操作?”
第四步:看 inline cache
dis.dis(
calculate,
show_caches=True
)
解决:
“哪些位置拥有运行时缓存能力?”
第五步:预热以后观察 specialization
for _ in range(100_000):
calculate(99.0, 5)
dis.dis(
calculate,
adaptive=True,
show_caches=True
)
解决:
“解释器根据真实运行模式做了什么优化?”
这五步,是非常值得加入 Python 高级开发者工具箱的一套方法。
十七、理解执行模型,对日常 Python 开发到底有什么用?
有人可能会问:
“我又不开发 CPython,为什么要知道这些?”
因为理解底层执行模型,会直接改变你理解代码的方式。
当你知道:
obj.name
不是免费的,就更容易理解为什么大量动态属性访问可能成为热点。
当你知道:
global_name
需要名称解析,就更容易理解 CPython 为什么要尝试 specialization LOAD_GLOBAL。
当你知道:
a + b
本质上是对象协议,而不是简单 CPU ADD,就更容易理解 Python 动态类型为什么强大、又为什么需要运行时成本。
当你理解:
source
→ AST
→ bytecode
→ frame
→ interpreter
很多过去看起来“玄学”的概念都会突然连起来:
- decorator;
- closure;
- traceback;
- profiler;
- debugger;
- coroutine;
- generator;
exec();compile();inspect;ast;dis;- 性能 profiling。
它们并不是彼此孤立的高级技巧,而是在同一套执行系统的不同位置观察 Python。
十八、最后建立一张完整的心智地图
回到最初:
def add(a, b):
return a + b
现在我们已经可以看到它背后的完整故事。
① Source Code
def add(a, b):
return a + b
↓
② Token / Parser
理解 def、参数、return、+
↓
③ AST
FunctionDef
└── Return
└── BinOp(+)
↓
④ Symbol Table
a → local
b → local
↓
⑤ Compiler / CFG
生成并优化指令序列
↓
⑥ Bytecode + Metadata
LOAD...
BINARY_OP
RETURN_VALUE
↓
⑦ Code Object
bytecode
constants
variables
source positions
stack information
...
↓
⑧ Function Object
add
├── __code__
├── __globals__
├── __defaults__
└── ...
↓
⑨ Call
add(1, 2)
↓
⑩ Frame
locals
stack
instruction pointer
globals
builtins
...
↓
⑪ CPython Interpreter
读取 opcode
执行 opcode
维护 Python object
↓
⑫ Adaptive Specialization
观察热点代码与真实类型
BINARY_OP
↓
BINARY_OP_ADD_INT
↓
⑬ CPU 最终执行 CPython 的底层机器指令
这才是“Python 代码跑起来”更接近真实世界的答案。
写在最后
Python 最迷人的地方之一,就是它可以同时拥有两个完全不同的世界。
在表面上:
def add(a, b):
return a + b
简单得几乎像一句自然语言。
而往下一层:
AST
Code Object
Bytecode
Frame
Eval Loop
Inline Cache
Quickening
Specialization
又足够一个开发者研究很多年。
也正因为如此,学习 Python 编程不能永远停留在“语法会不会写”。
当你开始追问:
dis.dis()为什么是这些指令?
__code__到底装了什么?
obj.x为什么能够被 specialization?
为什么一个函数刚启动和运行一段时间后的 bytecode 可能不一样?
你已经从“会使用 Python”,走向了“理解 Python”。
而这种理解最终会反馈到真正的 Python 实战中:你会更准确地分析性能、更从容地阅读框架源码,也更容易判断一个所谓的“Python 最佳实践”究竟是可靠经验,还是未经验证的性能玄学。
最后留两个问题:
你有没有遇到过某段 Python 代码第一次执行明显更慢、之后逐渐稳定的情况?最终找到的原因是什么?
如果解释器能够越来越聪明地根据运行时信息优化代码,那么未来 Python 的“解释执行”和“编译执行”之间,还会有多清晰的边界?
欢迎把你的实验结果、dis 输出和性能案例放在一起比较。真正理解 Python,往往就是从这些小小的“为什么”开始。🚀
参考与延伸阅读
本文涉及的 CPython 编译与执行细节主要可继续阅读:
- Python
dis官方文档:介绍 bytecode 反汇编、inline cache、adaptive bytecode,并特别说明字节码属于 CPython 实现细节。(Python documentation) - CPython Compiler Design:源码到 AST、instruction sequence、CFG、bytecode 和 code object 的完整编译过程。(GitHub)
- Python Data Model — Code Objects:了解
co_consts、co_varnames、co_stacksize等结构。(Python documentation) - PEP 659 — Specializing Adaptive Interpreter:深入理解 quickening、inline cache、specialization 与 de-optimization。(Python Enhancement Proposals (PEPs))
- CPython Bytecode Interpreter Internal Docs:继续深入 frame、evaluation loop 与 interpreter 内部结构。(GitHub)
**SEO 关键词:**Python编程、Python教程、Python实战、Python最佳实践、Python字节码、Python AST、CPython、dis模块、Code Object、Python性能优化、Specializing Interpreter

871

被折叠的 条评论
为什么被折叠?



