使用 Python 交换两个变量,通常有一种非常方便且 Pythonic 的方式:
a, b = b, a
而通常其他编程语言 (例如C/C++) 需要显式使用一个临时变量来存储中间值:
int temp;
temp = a;
a = b;
b = temp;
那么在 Python 中,通过 a, b = b, a 是如何实现变量交换的呢?
字节码对比
直接使用 Python 自带的 dis 模块来查看底层的字节码指令:
以下字节码基于 Python 3.13 运行,该版本引入了针对变量交换的特化指令优化。旧版本 Python 通常使用
ROT_TWO指令。
import dis
def swap_with_temp():
a = 1
b = 2
t = a
a = b
b = t
def swap_pythonic():
a = 1
b = 2
a, b = b, a
print("--- 使用临时变量 (temp) 的字节码 ---")
dis.dis(swap_with_temp)
print("\n--- 使用 a, b = b, a 的字节码 ---")
dis.dis(swap_pythonic)
使用 Python 3.13 运行如上代码,可以观察到不同的结果:
--- 使用临时变量 (temp) 的字节码 ---
3 RESUME 0
4 LOAD_CONST 1 (1)
STORE_FAST 0 (a)
5 LOAD_CONST 2 (2)
STORE_FAST 1 (b)
7 LOAD_FAST 0 (a)
STORE_FAST 2 (t)
8 LOAD_FAST 1 (b)
STORE_FAST 0 (a)
9 LOAD_FAST 2 (t)
STORE_FAST 1 (b)
RETURN_CONST 0 (None)
--- 使用 a, b = b, a 的字节码 ---
11 RESUME 0
12 LOAD_CONST 1 (1)
STORE_FAST 0 (a)
13 LOAD_CONST 2 (2)
STORE_FAST 1 (b)
15 LOAD_FAST_LOAD_FAST 16 (b, a)
STORE_FAST_STORE_FAST 16 (b, a)
RETURN_CONST 0 (None)
可以看到,使用临时变量的方法需要额外的存储和读取操作(STORE_FAST 2 (t) 和 LOAD_FAST 2 (t)),而 Pythonic 的方式只需要两条合并指令:
LOAD_FAST_LOAD_FAST- 一次性加载两个变量STORE_FAST_STORE_FAST- 一次性存储两个变量
指令详解
LOAD_FAST_LOAD_FAST
LOAD_FAST_LOAD_FAST 是一条合并指令,相当于连续执行两次 LOAD_FAST,其执行步骤是:
- 解码出第一个变量索引:
index1 = var_nums >> 4(高4位) - 解码出第二个变量索引:
index2 = var_nums & 15(低4位) - 把
co_varnames[index1]对应的值压栈 - 把
co_varnames[index2]对应的值压栈
STORE_FAST_STORE_FAST
STORE_FAST_STORE_FAST 的执行步骤是:
- 解码出第一个变量索引:
index1 = var_nums >> 4(高4位) - 解码出第二个变量索引:
index2 = var_nums & 15(低4位) - 把栈顶
STACK[-1]存入co_varnames[index1] - 把次栈顶
STACK[-2]存入co_varnames[index2]
参数编码
Python 字节码的设计由操作码和参数组成,无法携带两个独立的参数,所以将两个变量索引合并到一个字节 var_nums 中。
co_varnames 是 Python 存储局部变量名的一个元组。
在本例中,co_varnames 为 ('a', 'b'),即:
a的索引为 0b的索引为 1
对于 LOAD_FAST_LOAD_FAST,我们需要加载 b(索引 1)和 a(索引 0),所以参数计算为:
var_nums = (1 << 4) | 0 = 16
将索引 1 和索引 0 分别存储到高四位和低四位。
执行过程
现在跟踪一下完整的执行过程:
步骤 1:执行 LOAD_FAST_LOAD_FAST 16 (b, a)
此时栈的结构是:
--- top
| 1 | <-- 原变量 a 的值 (由第二个 LOAD 压入,索引0)
-----
| 2 | <-- 原变量 b 的值 (由第一个 LOAD 压入,索引1)
--- bottom
步骤 2:执行 STORE_FAST_STORE_FAST 16 (b, a)
值得注意的是:我们的源代码左侧是 a, b,而字节码参数却是 16(代表 b, a),这是由指令的定义和栈中数据的顺序决定的:
- 该指令将栈顶(Top)的值存入高4位索引(即
b)。当前栈顶是1(原a的值),所以b被赋值为1。 - 该指令将次栈顶(Second)的值存入低4位索引(即
a)。当前次栈顶是2(原b的值),所以a被赋值为2。
正是通过这种巧妙的参数构造和栈操作顺序,Python 在一条指令内完成了两个变量值的”交叉”写入,实现了交换。
性能优化与限制
由于 LOAD_FAST_LOAD_FAST 和 STORE_FAST_STORE_FAST 的参数 var_nums 是一个字节(8 位),高 4 位和低 4 位分别保存了两个变量索引,所以每个索引最多只能表示 0 - 15(共 16 个值)。
这意味着:当一个函数的局部变量超过 16 个时,Python 将退回到普通的 LOAD_FAST 和 STORE_FAST 指令,无法使用这种合并指令优化。好在大部分函数都不会超过 16 个局部变量,所以这个优化在大多数情况下都能生效。
总结
Python 通过 a, b = b, a 实现变量交换时,底层使用了特化的合并指令:
- 减少了指令数量(从 6 条减少到 2 条)
- 避免了临时变量的内存分配
- 提高了执行效率