Python-为什么只需一条语句abba就能直接交换两个变量

34次阅读

共计 2325 个字符,预计需要花费 6 分钟才能阅读完成。

从接触 Python 时起,我就感觉 Python 的元组解包(unpacking)挺有意思,十分简洁好用。

最不言而喻的例子就是多重赋值,即在一条语句中同时给多个变量赋值:

>>> x, y = 1, 2
>>> print(x, y)  # 后果:1 2

在此例中,赋值操作符“=”号的右侧的两个数字会被存入到一个元组中,即变成 (1,2),而后再被解包,顺次赋值给“=”号左侧的两个变量。

如果咱们间接写 x = 1,2,而后打印出 x,或者在“=”号右侧写成一个元组,就能证实到这一点:

>>> x = 1, 2
>>> print(x)     # 后果:(1, 2)
>>> x, y = (1, 2)
>>> print(x, y)  # 后果:1 2

一些博客或公众号文章在介绍到这个个性时,通常会顺着举一个例子,即基于两个变量,间接替换它们的值:

>>> x, y = 1, 2
>>> x, y = y, x
>>> print(x, y) # 后果:2 1

一般而言,替换两个变量的操作须要引入第三个变量。情理很简略,如果要替换两个杯子中所装的水,天然会须要第三个容器作为直达。

然而,Python 的写法并不需要借助两头变量,它的模式就跟后面的解包赋值一样。正因为这个模式类似,很多人就误以为 Python 的变量替换操作也是基于解包操作。

然而,事实是否如此呢?

我搜寻了一番,发现有人试图答复过这个问题,然而他们的答复根本不够全面。(当然,有不少是谬误的答案,还有更多人只是知其然,却从未想过要知其所以然)

先把本文的答案放进去吧:Python 的替换变量操作不齐全基于解包操作,有时候是,有时候不是!

有没有感觉这个答案很神奇呢?是不是闻所未闻?!

到底怎么回事呢?先来看看题目中最简略的两个变量的状况,咱们上 dis 大杀器看看编译的字节码:

上图开了两个窗口,能够不便比拟“a,b=b,a”与“a,b=1,2”的不同:

  • “a,b=b,a”操作:两个 LOAD_FAST 是从部分作用域中读取变量的援用,并存入栈中,接着是最要害的 ROT_TWO 操作,它会替换两个变量的援用值,而后两个 STORE_FAST 是将栈中的变量写入部分作用域中。
  • “a,b=1,2”操作:第一步 LOAD_CONST 把“=”号右侧的两个数字作为元组放到栈中,第二步 UNPACK_SEQUENCE 是序列解包,接着把解包后果写入部分作用域的变量上。

很显著,模式类似的两种写法实际上实现的操作并不相同。在替换变量的操作中,并没有装包和解包的步骤!

ROT_TWO 指令是 CPython 解释器实现的对于栈顶两个元素的快捷操作,扭转它们指向的援用对象。

还有两个相似的指令是 ROT_THREE 和 ROT_FOUR,别离是快捷替换三和四个变量(摘自:ceval.c 文件,最新的 3.9 分支):

预约义的栈顶操作如下:

查看官网文档中对于这几个指令的解释,其中 ROT_FOUR 是 3.8 版本新加的:

  • ROT_TWO

    Swaps the two top-most stack items.

  • ROT_THREE

    Lifts second and third stack item one position up, moves top down to position three.

  • ROT_FOUR

    Lifts second, third and forth stack items one position up, moves top down to position four.
    New in version 3.8.

CPython 应该是认为这几种变量的替换操作很常见,因而才提供了专门的优化指令。就像 [-5,256] 这些小整数被事后放到了整数池里一样。

对于更多变量的替换操作,实际上则会用到后面说的解包操作:

截图中的 BUILD_TUPLE 指令会将给定数量的栈顶元素创立成元组,而后被 UNPACK_SEQUENCE 指令解包,再顺次赋值。

值得一提的是,此处之所以比后面的“a,b=1,2”多出一个 build 操作,是因为每个变量的 LOAD_FAST 须要先独自入栈,无奈间接被组合成 LOAD_CONST 入栈。也就是说,“=”号右侧有变量时,不会呈现前文中的 LOAD_CONST 一个元组的状况。

最初还有一个值得一提的细节,那几个指令是跟栈中元素的数量无关,而不是跟赋值语句中理论替换的变量数无关。看一个例子就明确了:

剖析至此,你应该明确前文中的论断是怎么回事了吧?

咱们略微总结一下:

  • Python 能在一条语句中实现多重赋值,这是利用了序列解包的个性
  • Python 能在一条语句中实现变量替换,不需引入两头变量,在变量数少于 4 个时(3.8 版本起是少于 5 个),CPython 是利用了 ROT_* 指令来替换栈中的元素,当变量数超出时,则是利用了序列解包的个性。
  • 序列解包是 Python 的一大个性,然而在本文的例子中,CPython 解释器在小小的操作中还提供了几个优化的指令,这相对会超出大多数人的认知

如果你感觉本文剖析得不错,那你应该会喜爱这些文章:

1、Python 为什么应用缩进来划分代码块?

2、Python 的缩进是不是反人类的设计?

3、Python 为什么不必分号作语句终止符?

4、Python 为什么没有 main 函数?为什么我不举荐写 main 函数?

5、Python 为什么举荐蛇形命名法?

6、Python 为什么不反对 i++ 自增语法,不提供 ++ 操作符?

写在最初:本文属于“Python 为什么”系列(Python 猫出品),该系列次要关注 Python 的语法、设计和倒退等话题,以一个个“为什么”式的问题为切入点,试着展示 Python 的迷人魅力。局部话题会推出视频版,请在 B 站收看,观看地址:视频地址

公众号【Python 猫 】,本号连载优质的系列文章,有 Python 为什么系列、喵星哲学猫系列、Python 进阶系列、好书举荐系列、技术写作、优质英文举荐与翻译等等,欢送关注哦。

正文完
 0