Python 程序员肯定知道
a,b = b,a
,这句话用来交换两个变量。相较于其它语言需要引入一个
temp
来临时存储变量的做法,
Python
的这种写法无疑非常优雅。
佶屈聱牙的 C 写法:
int a = 1;
int b = 2;
int temp;
temp = a;
a = b;
b = temp;
简洁优雅的 Python 写法:
a,b = 1,2
a,b = b,a
虽然语法非常方便,但我们始终不曾想过:它是怎么运作的?背后支撑它的机制是什么?下面让我们一步步分析它。
通俗的说法
最常见的解释是:
a,b = b,a
中右侧是元组表达式,即
b,a
是一个两个元素的
tuple(a,b)
。表达式左侧是两个待分配元素,而
=
相当于元组元素拆包赋值操作。
这种方法,理解起来最简单,但实际是这种情况么?
让我们从字节码上看下,是不是这种情况。
从字节码一窥交换变量
大家可能不太了解 Python 字节码。Python 解释器是一个基于栈的虚拟机。Python 解释器就是编译、解释 Python 代码的二进制程序。
虚拟机是一种执行代码的容器,相较于二进制代码具有方便移植的特点。而 Python 的虚拟机就是栈机器。
Python 中函数调用、变量赋值等操作,最后都转换为对栈的操作。这些对栈的具体操作,就保存在字节码里。
dis
模块可以反编译字节码,使其变成人类可读的栈机器指令。如下,我们看反编译
a,b=b,a
的代码。
>>> import dis
>>> dis.dis("a,b=b,a")
1 0 LOAD_NAME 0 (b)
2 LOAD_NAME 1 (a)
4 ROT_TWO
6 STORE_NAME 1 (a)
8 STORE_NAME 0 (b)
10 LOAD_CONST 0 (None)
12 RETURN_VALUE
可见,在 Python 虚拟机的栈上,我们按照表达式右侧的
b,a
的顺序,先后压入计算栈中,然后用一个重要指令
ROT_TWO
,这个操作交换了
a
和
b
的位置,最后
STORE_NAME
操作将栈顶的两个元素先后弹出,传递给
a
和
b
元素。
栈的特性是先进后出(
FILO
)。当我们按b,a顺序压入栈的时候,弹出时先出的就是a,再弹出就是b。
STORE_NAME
指令会把栈顶元素弹出,并关联到相应变量上。
如果没有第 4 列的指令
ROT_TWO
,此次
STORE_NAME
弹出的第一个变量会是后压栈的 a,这样就是
a=a
的效果。有了
ROT_TWO
则完成了变量的交换。
好了,我们知道靠压栈、弹栈和交换栈顶的两个元素,实现了
a,b = b,a
的操作。
同时,我们也知道了,上诉元组拆包赋值的说法,是不恰当的。
那
ROT_TWO
是怎么具体操作的呢?
后台怎么执行?
见名知意,可以猜出来
ROT_TWO
是交换两个栈顶变量的操作。在 Python 源代码的层面上,来看是如何交换两个栈顶的元素。
下载 Python 源代码,进入
Python/ceval.c
文件,在 1101 行,我们看到了
ROT_TWO
的操作。
TARGET(ROT_TWO){
PyObject *top = TOP();
PyObject *second = SECOND();
SET_TOP(second);
SET_SECOND(top);
FAST_DISPATCH();
}
代码比较简单,我们用
TOP
和
SECOND
宏获取了栈上的 a,b 元素,然后再用
SET_TOP、SET_SECOND
宏把值写入栈中。以此完成交换栈顶元素的操作。