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

2020-12-16 CDA數據分析師

Python是一款使用方便,易上手的工具,我們平常在工作中經常會用到,而且同時也是一款功能強大的程式語言,被廣泛應用於數據分析、web開發、人工智慧等行業。但是無論行業,哪個領域,想要熟練使用Python,就必須掌握Python的基礎知識。

以下文章來源於:微信公眾號Python貓

作者: 豌豆花下貓

從接觸 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_TWOSwaps the two top-most stack items.ROT_THREELifts second and third stack item one position up, moves top down to position three.ROT_FOURLifts 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編程第7課,賦值語句高階練習,4種方法交換兩個變量的值
    5課:累加器,變量與賦值進階練習Python編程第6課:15個編程好習慣之一,使用注釋符前面6課,我們主要是學習並鞏固練習Python的變量、表達式、賦值語句以及輸出函數。本節課我們繼續進行賦值語句的高階練習,熟練掌握Python中交換兩個變量值的4種方法。第1種方法:使用元組交換(Python特有的語法)格式:a,b=b,a如圖7.1所示
  • 第11節:變量的定義與賦值語句
    從上述可以看出,程序的兩個要素是:對象和行為。如果把對象看作是單片機的RAM數據存儲器,那麼行為就是單片機的ROM程序存儲器。如果把對象看作是變量,那麼行為就是指令語句。其中b就是右邊對象。其中分號「;」代表一條語句的結束符。賦值語句與ROM的內在關係。賦值語句是行為,凡是程序的行為指令都存儲在單片機的ROM區。C編譯器會把一條賦值語句翻譯成對應的一條或者幾條機器碼,機器碼指令也是以字節為單位的。下載程序的時候,這些機器碼就會被下載進單片機的ROM區。
  • Python基礎教程判斷(if)語句
    我們可以把整個 if 語句看成一個完整的代碼塊2.2 判斷語句演練 —— 判斷年齡需求定義一個整數變量記錄年齡定義年齡變量age = 182. 判斷是否滿 18 歲if 語句以及縮進部分的代碼是一個完整的代碼塊if age >= 18: print("可以進網吧嗨皮……")3. 思考!
  • Python 基本數據類型和變量
    如上面的代碼截圖,以 # 開頭的語句是注釋,其他每一行都是一個語句,當語句以冒號 : 結尾時,縮進的語句視為代碼塊。要注意的是 Python 程序是大小寫敏感的,如果寫錯了大小寫,程序會報錯。說明你的語句不合規則。
  • python基礎知識變量、運算和數據類型
    昨天搭建好juypter notebook,並錄製了第一堂python的課程,簡單講解了python的歷史,以及python可以幫助我們如何提供效率。布爾值布爾值只有True和 False 兩個值;布爾值在數字運算中,True代表1,False代表0;關係運算和邏輯運算返回的就是布爾值,如 1>2 等。
  • python變量類型,列表和元組
    微信公眾號:學點啥玩點啥小白友好型python變量類型,列表和元組# -*- coding: utf-8 -*-"""Created on Mon Jan 25 12:25:55 2021@author: sd"""#第2章.變量和簡單數據類型#變量#1.字符串
  • 初學者專題:變量和賦值
    比如本文,就是要幫助學習者,對變量和賦值這兩個非常基本、幾乎無處不在的內容作為一個專題進行總結。當執行了a = 1.23之後,本來內存中已經創建了1.23這個對象,但是,當再次執行id(1.23)時,因為兩個不完全一樣,交互模式的解析器忘記了前面的1.23,於是乎又在存儲器中重新創建了id(1.23)中的1.23對象。可為什麼a = 2不如此呢?因為Python還有一個習慣,把-256~256這些整數,在內存中有「常住戶口」。
  • C代碼交換a、b值不一樣的寫法
    本文轉載自【微信公眾號:strongerHuang,ID:strongerHuang】經微信公眾號授權轉載,如需轉載與原文作者聯繫交換a、b的值在C語言的學習中是很常見的問題。最常用的方法就是引入一個中間變量當作中間介質來交換a、b的值。
  • 09.為什麼Python循環語句裡也有else?
    2. if flag 和 for j 語句縮進保持一致,才能保證兩個語句的地位相等。if 語句始終被執行。反證法,如果 x 不是質數,x = a * b,那麼 a,b顯然也能整除n,且都比x小,由此可證假設不成立。)
  • Python基礎入門:基礎語法和變量類型
    """輸入輸出通常是一條語句一行,如果語句很長,我們可以使用反斜槓(\)來實現多行語句。在 [], {}, 或 ()中的多行語句,則不需要反斜槓。sentence1 = "I love " + \"python"sentence2 = ["I", "love",          "python"]另外,我們也可以同一行顯示多條語句,語句之間用分號(;)分割,示例如下:print('Hello');print('world')
  • Python基礎
    我們可以使用斜槓( \)將一行的語句分為多行顯示.python中單行注釋採用 # 開頭。python 中多行注釋使用三個單引號(''')或三個雙引號(""")。縮進相同的一組語句構成一個代碼塊,我們稱之代碼組。
  • python進階教程之變量
    b=a變量 b是第 2 個貼在數字>如果沒有,查找 函數外部 是否存在 指定名稱 的全局變量,如果有,直接使用如果還沒有,程序報錯!1) 函數不能直接修改 全局變量的引用提示:在其他的開發語言中,大多 不推薦使用全局變量 —— 可變範圍太大,導致程序不好維護!
  • python教程之python數學運算
    存儲數據變量#兩個變量交換值,通過第三個參數實現print('兩個變量交換值,通過第三個參數實現'); #第一種方法是通過第三個參數實現x=5;#變量賦值y=11; #變量賦值print('交換前',x,y);temp=x;x=y;y=temp;print('交換後',x,y);#兩個變量交換值,更便捷的方法print('兩個變量交換值
  • 一起學python-認識神秘的循環語句「if」
    昨天我們學習了變量的概念,還記得變量是個什麼東西嗎,如果忘記了請看看上一篇的內容,溫故而知新噢。今天給大家分享下python中的循環語句 if 我們人的一生中會面臨著很多選擇,實際上每天每時每刻我們的大腦都在做出這樣或者那樣的選擇。
  • Python基礎:數據類型和變量&字符串和編碼
    在Python中,等號=是賦值語句,可以把任意數據類型賦值給變量,同一個變量可以反覆賦值,而且可以是不同類型的變量,例如:aa = 123 # a是整數print(a)a = 'ABC' # a變為字符串print(a)這種變量本身類型不固定的語言稱之為動態語言,與之對應的是靜態語言。
  • Python專題 | (三)注釋、變量與輸出
    這一點隨著大家編程經驗的增加就能逐漸有自己的感悟,能明白哪裡應該有什麼樣的注釋。在編程前期,寫下變量、函數(之後會講到)和自己掌握不太清晰的語句的注釋就可以了。python注釋的寫法有很多種,我們分為兩種情況介紹:單行注釋一般使用「#」號開頭,表示注釋開始,#號之後的都是注釋,在這裡寫的所有語句均不會被計算機識別和執行。
  • 編程入門第六課,交換語句(switch,case)
    介紹1.1 介紹上一課我們學會了循環語句(for,while)的編程技巧,這使得我們可以開發比較複雜的程序了。今天福哥會給童鞋們講講交換語句(switch,case)的使用方法,交換語句適合對一個變量的值等於不同內容的時候進行不同處理的情況。
  • Python基本語法與變量 - 米粒教育
    例1:由於縮進沒有對齊而產生的語法錯誤#IF語句示例 a=input("請輸入第一個數")b=input("請輸入第二個數")>if a > b:print("a>b")else:print("a<b")else語句的print函數和if語句的print函數沒有縮進對齊,產生語法錯誤。
  • Python基礎(一)
    ;range(a, b)返回一個序列,從a開始到b為止,但不包括b,range默認步長為1,可以指定步長,range(0,10,2);(7)break語句終止循環語句,如果從for或while中終止,任何對應循環的else將不執行。
  • javaScript入門(1)注釋、語句結束符、變量、常量、標識符
    注釋// 這是單行/* 這是多行注釋*/語句結束符JavaScript中,每一個語句的結束都應該使用分號;var 變量名1= 變量值1, 變量名2= 變量值2, 變量名3= 變量值3; // 聲明多個變量用逗號隔開,語句以分號結束變量提升在ES6中,如果我們在使用var關鍵字初始化一個變量之前調用了這個變量,那麼調用這個變量的時候並不會報錯