Python程序员一定知道a,b=b,a,这句话是用来交换两个变量的。相对于其他需要引入一个temp来临时存放变量的语言,Python的这种写法无疑是非常优雅的。简洁大方的C写法:inta=1;intb=2;inttemp;temp=a;a=b;b=temp;简洁优雅的Python写法:a,b=1,2a,bb=b,虽然a的语法很方便,但是我们从来没有想过:它是如何工作的呢?它背后的机制是什么?让我们一步一步来分析。口语化最常见的解释是:a,b=b,a是右边的元组表达式,即b,a是二元元组(a,b)。表达式左边是两个待赋值的元素,=相当于元组元素的拆包赋值操作。这种方法最容易理解,但是真的是这样吗?我们从字节码来看是不是这样。从字节码交换变量一瞥您可能对Python字节码知之甚少。Python解释器是一个基于堆栈的虚拟机。Python解释器是编译和解释Python代码的二进制程序。虚拟机是执行代码的容器,比二进制代码更便携。Python虚拟机是一个栈机。Python中的函数调用、变量赋值等操作,最终都转化为对栈的操作。这些对堆栈的具体操作都存储在字节码中。dis模块将字节码反编译为人类可读的堆栈机器指令。如下,我们看a,b=b,a的反编译代码。>>>importdis>>>dis.dis("a,bb=b,a")10LOAD_NAME0(b)2LOAD_NAME1(a)4ROT_TWO6STORE_NAME1(a)8STORE_NAME0(b)10LOAD_CONST0(None)12RETURN_VALUE在Python的堆栈上可见virtualmachine上面我们先把表达式右边的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宏将值写入堆栈。这样就完成了交换栈顶元素的操作。评价顺序怪现象!接下来我们来看一个奇怪的现象,在这篇文章中也能看到。如下,我们尝试对这个列表进行排序:>>>a=[0,1,3,2,4]>>>a[a[2]],a[2]=a[2],a[a[2]]>>a>>>[0,1,2,3,4]>>>a=[0,1,3,2,4]>>>a[2],a[a[2]]=a[a[2]],a[2]>>>a>>>[0,1,3,3,4]根据理解a,b=b,aandb,a=a,b是相同的结果,但是从上面的例子我们看到两者的结果是不一样的。造成这种现象的原因是:评价顺序。毫无疑问,整个表达式首先对右边的两个元素求值,然后将它们保存为常量。最后赋值给左边的两个变量。在最后赋值的时候,需要注意的是我们是从左到右依次赋值的。如果先修改a[2],势必会影响后面的a[a[2]]的列表下标。“你可以用反汇编代码来分析产生这种现象的具体步骤。”奇怪变回开箱现象!!当我们用常量作为右元组给左变量赋值时;或者使用三个以上的元素,来完成方便的交换,不是字节码级别的ROT_TWO这样的操作。>>>dis.dis(“a,b,c,d=b,c,d,a”)10load_name3load_name6load_name9name9loads12build_tuple_tuple_tuple15unpack_sequence18store_name21store_name21store_name21store_name24store_name_name_name_name_name_name_name_name_name_name_nameloadietlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentlentley>到左边的变量。上面说的那句俗语在这里又是对的!也就是说,当交换的元素少于四个时,Python使用优化的栈操作来完成交换。当使用常量或超过四个元素时,使用元组拆包赋值的方式进行交换。至于为什么有四个元素,应该是因为Python最多支持ROT_THREE操作。如果有四个元素,系统不知道如何优化它。但是在新版的Python中,看到了ROT_FOUR操作,所以这时候,四元素还是通过ROT_*操作来优化的。>>>importopcode>>>opcode.opmap["ROT_THREE"]3在这个例子中,这个版本的Python支持ROT_THREE操作。你也可以使用ROT_FOUR来检查你的Python是否支持它,然后确定是否可以轻松交换四个以上的元素。
