Python 爲何只需一條語句「a,b=b,a」,就能直接交換兩個變量?

從接觸 Python 時起,我就以爲 Python 的元組解包(unpacking)挺有意思,很是簡潔好用。python

最顯而易見的例子就是多重賦值,即在一條語句中同時給多個變量賦值:ide

>>> 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

一些博客或公衆號文章在介紹到這個特性時,一般會順着舉一個例子,即基於兩個變量,直接交換它們的值:ui

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

通常而言,交換兩個變量的操做須要引入第三個變量。道理很簡單,若是要交換兩個杯子中所裝的水,天然會須要第三個容器做爲中轉。spa

然而,Python 的寫法並不須要藉助中間變量,它的形式就跟前面的解包賦值同樣。正由於這個形式類似,不少人就誤覺得 Python 的變量交換操做也是基於解包操做。翻譯

可是,事實是否如此呢?設計

我搜索了一番,發現有人試圖回答過這個問題,可是他們的回答基本不夠全面。(固然,有很多是錯誤的答案,還有更多人只是知其然,卻從未想過要知其因此然)code

先把本文的答案放出來吧: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 解釋器在小小的操做中還提供了幾個優化的指令,這絕對會超出大多數人的認知

若是你以爲本文分析得不錯,那你應該會喜歡這些文章:

一、Python爲何使用縮進來劃分代碼塊?

二、Python 的縮進是否是反人類的設計?

三、Python 爲何不用分號做語句終止符?

四、Python 爲何沒有 main 函數?爲何我不推薦寫 main 函數?

五、Python 爲何推薦蛇形命名法?

六、Python 爲何不支持 i++ 自增語法,不提供 ++ 操做符?

寫在最後:本文屬於「Python爲何」系列(Python貓出品),該系列主要關注 Python 的語法、設計和發展等話題,以一個個「爲何」式的問題爲切入點,試着展示 Python 的迷人魅力。部分話題會推出視頻版,請在 B 站收看,觀看地址:視頻地址

公衆號【Python貓】, 本號連載優質的系列文章,有Python爲何系列、喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫做、優質英文推薦與翻譯等等,歡迎關注哦。

相關文章
相關標籤/搜索