1. 程式人生 > >Python 為什麼只需一條語句“a,b=b,a”,就能直接交換兩個變數?

Python 為什麼只需一條語句“a,b=b,a”,就能直接交換兩個變數?

從接觸 Python 時起,我就覺得 Python 的元組解包(unpacking)挺有意思,非常簡潔好用。 最顯而易見的例子就是多重賦值,即在一條語句中同時給多個變數賦值: ```python >>> x, y = 1, 2 >>> print(x, y) # 結果:1 2 ``` 在此例中,賦值操作符“=”號的右側的兩個數字會被存入到一個元組中,即變成 (1,2),然後再被解包,依次賦值給“=”號左側的兩個變數。 如果我們直接寫`x = 1,2` ,然後打印出 x,或者在“=”號右側寫成一個元組,就能證實到這一點: ```python >>> x = 1, 2 >>> print(x) # 結果:(1, 2) >>> x, y = (1, 2) >>> print(x, y) # 結果:1 2 ``` 一些部落格或公眾號文章在介紹到這個特性時,通常會順著舉一個例子,即基於兩個變數,直接交換它們的值: ```python >>> x, y = 1, 2 >>> x, y = y, x >>> print(x, y) # 結果:2 1 ``` 一般而言,交換兩個變數的操作需要引入第三個變數。道理很簡單,如果要交換兩個杯子中所裝的水,自然會需要第三個容器作為中轉。 然而,Python 的寫法並不需要藉助中間變數,它的形式就跟前面的解包賦值一樣。正因為這個形式相似,很多人就誤以為 Python 的變數交換操作也是基於解包操作。 但是,事實是否如此呢? 我搜索了一番,發現有人試圖回答過這個問題,但是他們的回答基本不夠全面。(當然,有不少是錯誤的答案,還有更多人只是知其然,卻從未想過要知其所以然) 先把本文的答案放出來吧:**Python 的交換變數操作不完全基於解包操作,有時候是,有時候不是!** 有沒有覺得這個答案很神奇呢?是不是聞所未聞?! 到底怎麼回事呢?先來看看標題中最簡單的兩個變數的情況,我們上`dis` 大殺器看看編譯的位元組碼: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1ggpm8nf7soj20y80dvt9q.jpg) 上圖開了兩個視窗,可以方便比較“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 分支): ![](http://ww1.sinaimg.cn/large/68b02e3bgy1ggpo6w29pxj20hh0hhq3s.jpg) 預定義的棧頂操作如下: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1ggpoi0nkscj20l50an3zd.jpg) 檢視官方文件中對於這幾個指令的解釋,其中 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] 這些小整數被預先放到了整數池裡一樣。 對於更多變數的交換操作,實際上則會用到前面說的解包操作: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1ggppuj44dfj20hy0dhaai.jpg) 截圖中的 BUILD_TUPLE 指令會將給定數量的棧頂元素建立成元組,然後被 UNPACK_SEQUENCE 指令解包,再依次賦值。 值得一提的是,此處之所以比前面的“a,b=1,2”多出一個 build 操作,是因為每個變數的 LOAD_FAST 需要先單獨入棧,無法直接被組合成 LOAD_CONST 入棧。也就是說,“=”號右側有變數時,不會出現前文中的 LOAD_CONST 一個元組的情況。 最後還有一個值得一提的細節,那幾個指令是跟棧中元素的數量有關,而不是跟賦值語句中實際交換的變數數有關。看一個例子就明白了: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1ggpr5j60odj20hy090aa7.jpg) 分析至此,你應該明白前文中的結論是怎麼回事了吧? 我們稍微總結一下: - Python 能在一條語句中實現多重賦值,這是利用了序列解包的特性 - Python 能在一條語句中實現變數交換,不需引入中間變數,在變數數少於 4 個時(3.8 版本起是少於 5 個),CPython 是利用了 ROT_* 指令來交換棧中的元素,當變數數超出時,則是利用了序列解包的特性。 - 序列解包是 Python 的一大特性,但是在本文的例子中,CPython 直譯器在小小的操作中還提供了幾個優化的指令,這絕對會超出大多數人的認知 如果你覺得本文分析得不錯,那你應該會喜歡這些文章: 1、[Python為什麼使用縮排來劃分程式碼塊?](https://mp.weixin.qq.com/s/byhJnKoKSDnhUNUE9WWopw) 2、[Python 的縮排是不是反人類的設計?](https://mp.weixin.qq.com/s/pi1x6lT88dMmfUUqcVet-A) 3、[Python 為什麼不用分號作語句終止符?](https://mp.weixin.qq.com/s/LZ2ocKBfYurJtllU4asP2Q) 4、[Python 為什麼沒有 main 函式?為什麼我不推薦寫 main 函式?](https://mp.weixin.qq.com/s/1ehySR5NH2v1U8WIlXflEQ) 5、[Python 為什麼推薦蛇形命名法?](https://mp.weixin.qq.com/s/U4n3aEhznPx7lJ8lj6rU_A) 6、[Python 為什麼不支援 i++ 自增語法,不提供 ++ 操作符?](https://mp.weixin.qq.com/s/Bdn5XPUkH9Ssz4UZc4NKvQ) 寫在最後:本文屬於“Python為什麼”系列(Python貓出品),該系列主要關注 Python 的語法、設計和發展等話題,以一個個“為什麼”式的問題為切入點,試著展現 Python 的迷人魅力。部分話題會推出視訊版,請在 B 站收看,觀看地址:[視訊地址](https://space.bilibili.com/97566624/video) ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gfffh3g28lj2076076q3e.jpg) 公眾號【**Python貓**】, 本號連載優質的系列文章,有Python為什麼系列、喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關