=不是運算器 (演算子);才是--談指派敘述


熟悉 C 家族語言的人可能會把 Python的 =當成是運算器 (演算子)不過實際上它並不是運算器, 所以不會有運算結果, =指派敘述 (assigment statement)語法的一部分, 因此, 不能像是這樣把指派敘述放在運算式內:
>>> a = (b = 2)
  File "<stdin>", line 1
    a = (b = 2)
           ^
SyntaxError: invalid syntax
>>>
如果是 C 語言, 這會把變數 b設為 2再把 a設定為 b = 2的 2 .但是在 Python中, 因為 b = 2並不是運算式, 不符合指派敘述等號右邊的語法規定, 所以只會噴出語法錯誤的訊息.

多重指派


在指派敘述中, 等號左邊稱為標的清單 (対象リスト)當標的清單只有單一標的時, 就是非常直覺的命名並綁定, 我們在 一文中解釋過, 這裡我們要談的是指派敘述的各種變化, 也就是標的清單內有多個標的的情況.
如果標的清單中有多個標的, 那麼在等號右側也必須要有對應數量的物件, 實際上就會一一對應進行綁定, 例如:
>>> a, b = 1, 2
>>> a
1
>>> b
2
如果數量不對, 就會噴出錯誤訊息:
>>> a, b, c = 1, 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 3, got 2)
>>>
錯誤訊息告訴我們物件數量不足, 需要 3個, 但是只有提供 2個.或是反過來, 物件數量比標的多:
>>> a, b = 1, 2, 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)
>>>
也一樣會噴出錯誤.如果等號右邊只有一個物件時, 你會看到噴出的訊息不一樣:
>>> a, b = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot unpack non-iterable int object
>>>
這裡的訊息是說無法對不可走訪 (非iterable )的 int物件進行 アンパック我們分兩段來說明.
對於標的清單中有多個標的的情況, 等號右邊必須要是一個可走訪的物件, 所謂可走訪的物件就是可以循序一一取出內含物件的容器物件, 像是串列、元組、字典都是, 也就是可以放到 for敘述的物件, 要分辨物件是否可走訪, 只要看它是否具有 __iter__方法即可, 例如:
>>> (2).__iter__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute '__iter__'. Did you mean: '__str__'?
>>> (1, 2).__iter__
<method-wrapper '__iter__' of tuple object at 0x00000211F4B1EFC0>
>>>
你可以看到整數沒有 __iter__方法, 而元組有, 所以元組是可走訪的物件.多重指派的運作方式就是一一從可走訪的物件中取出內含的物件, 指派給對應的標的, 這個動作就稱為拆箱 (アンパック)
回過頭來看剛剛等號右側只有 1的例子, 因為它是 int物件, 不能走訪, 當然就無法拆箱, 所以錯誤訊息會說無法對不可走訪 (非iterable )的 int物件進行 アンパック.我們可以在 1的後面加上逗號, Python就會認為這是一個只有單一項目的元組:
>>> a, b = 1,
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 2, got 1)
>>>
錯誤訊息就變成是物件數量不足了.
由於元組實際上是以逗號區隔項目來表示, 常見的小括號並不是元組的一部分, 只是為了和其他物件隔開避免混淆, 因此底下兩種寫法都可以:
>>> a, b = 1, 2
>>> a, b = (1, 2)
由於只要是可走訪的物件都可以用來設定多重標的, 所以也可以使用串列、字典, 甚至字串, 例如:
>>> a, b = [1, 2]
>>> a
1
>>> b
2
>>> a, b = {10:1, 20:2}
>>> a
10
>>> b
20
>>> a, b = "AB"
>>> a
'A'
>>> b
'B'
>>>
要注意的是走訪字典時取出的是索引鍵, 而走訪字串取出的是個別的字元.

強制拆箱


如果等號右邊的物件內含容器物件, 我們希望能取出其中的物件指派給標的, 可以在物件前加上星號 *, 強制拆箱, 例如:
>>> a, b, c = 1, *(2, 3)
>>> a
1
>>> b
2
>>> c
3
>>>
本來等號右側只有 2個項目, 但因為加了星號在第 2個項目前, 所以會強制拆箱, 就變成總共 3個項目, 與標的清單內的數量相符:
a, b, c = 1, *(2, 3)
                |
                v
a, b, c = 1,  2, 3
如此即可一一對應指派了.

加星號 (星の)的指派標的


如果希望將物件中的部分項目自動組成串列指派給標的, 可以在標的前面加上星號 '*', 這樣會先從頭對應項目到加星號的標的之前, 然後再從尾端對應項目到加星號的標的之後, 再將剩下尚未對應到的項目組成一個串列後指派給加星號的標的, 例如:
>>> a, *b, c = 1, 2, 3, 4
>>> a
1
>>> b
[2, 3]
>>> c
4
>>>
實際對應如下:
a, *b,   c
|        |
v        v
1, 2, 3, 4
從頭對應, 1 -> A ;從尾端對應, 4 -> c ;最夠剩下 2 , 3組成串列對應到加了星號的 b .這個和拆箱相反的動作一般稱為裝箱 (パック)雖然在官方文件中好像沒有正式的名稱.
由於剩下未對應到的項目會組成一個串列, 只會對應到單一個標的, 因此在標的清單中只能有一個加星號的標的, 否則會出現錯誤:
>>> a, *b, *c = 1, 2, 3, 4
  File "<stdin>", line 1
SyntaxError: multiple starred expressions in assignment
>>>
指派敘述允許未對應的項目是 0個, 這時會組成空的串列, 例如:
>>> a, *b, c = 1, 2
>>> a
1
>>> b
[]
>>> c
2
>>>
所以等號右邊的項目數量可以比標的清單中的標的數量少一個.

用小括號或中括號幫標的分組


如果等號右邊是結構複雜的容器, 我們也可以使用小括號或是中括號建構具有階層結構的標的清單, 實際對應時會以遞迴的方式一層層完成指派.例如:
>>> a, (b, c) = 1, (2, 3)
>>> a
1
>>> b
2
>>> c
3
>>>
一開始的對應關係如下:
a, (b, c)
|    |
v    v
1, (2, 3)
遞迴變成 2個指派敘述:
a = 1
b, c = 2, 3
同樣的道理, 即使多層也沒有問題:
>>> (a, (b, c)), d = (1, (2, 3)), 4
>>> a
1
>>> b
2
>>> c
3
>>> d
4
>>>
原始對應關係如下:
(a, (b, c)), d
     |       |
     v       v
(1, (2, 3)), 4
遞回成 2個指派敘述:
a, (b, c) = 1, (2, 3)
d = 4
再遞迴成 3個指派敘述:
a = 1
b, c = 2, 3
d = 4
你也可以使用中括號來幫標的建立階層:
>>> a, [b, c] = 1, (2, 3)
>>> a
1
>>> b
2
>>> c
3
>>>
甚至混用兩種括號也沒問題:
>>> (a, [b, c]), d = (1, (2, 3)), 4
>>> a
1
>>> b
2
>>> c
3
>>> d
4
>>>
在個別階層內一樣可以使用加上星號的標的, 例如:
>>> a, *b, (c, *d), e = 1, 2, 3, (4, ), 5
>>> a
1
>>> b
[2, 3]
>>> c
4
>>> d
[]
>>> e
5
>>>
原始對應關係如下:
a, *b,  (c, *d),  e
|          |      |
v          v      v
1, 2, 3, (4, ),   5
所以:
a = 1
b = [2, 3]
c, *d = 4,
e = 5
最後:
c = 4
d = []
你可能會想說前面不是提到標的清單中只能有一個加上星號的標的, 但這個例子裡卻有兩個標的加上星號?其實我們如果看一開始的指派敘述, 它的標的清單裡只有 4個標的, 分別如下:
a
*b
(c, *d)
e
其中只有一個有加星號.要遞迴拆解到下一層的指派敘述時, 才會看到另一個加星號的標的:
c, *d = 4,
在這個指派敘述中, 一樣符合只有一個加上星號的標的.

利用多重指派交換內容


我們可以利用多重指派的方式完成內容互換的工作, 像是:
>>> a = 1
>>> b = 2
>>> a, b = b, a
>>> a
2
>>> b
1
>>>
這看起來好像很神奇, 但只要你了解 Python的綁定觀念, 就不會覺得驚訝.上面的過程可以分成以下步驟:

  • 一開始綁定 a Eye b :
    >>> a = 1
    >>> b = 2
    
     ___ ___ 
    | a | b |
      |   |  
      v   v
      1   2 
    

  • 接著依照等號右邊產生一個內含 2個項目的元組, 內含項目依序綁定到 ba綁定的物件:
    >>> a, b = b, a
     ___ ___ 
    | a | b |
      |   |      ___ ___             
      v   v     | . | . |            
      1   2       |   |        
      ^   ^       |   |
      |   |_______|   |
      |_______________|    
    

  • 再將 a循元組索引 0的項目綁訂到新的物件、 b循元組索引 1的項目綁訂到新的物件:
    >>> a, b = b, a
     ___ ___ 
    | a | b |
      |   |___________             
      |___________    |      
                  |   | 
      1   2       |   |        
      ^   ^       |   |
      |   |_______|   |
      |_______________|    
    
  • 最後 ab就互換綁定的物件了.
  • 注意指派時的順序


    如果在標的清單中有某個標的會使用到其他標的來當成容器的索引或是取得切片, 就要注意指派時會從清單中由左至右進行, 例如:
    >>> b = [1, 2]
    >>> a = 0
    >>> a, b[a] = 1, 4
    >>> a
    1
    >>> b
    [1, 4]
    >>>
    
    ab[a]的指派並不是同時完成的, 而是由左至右的順序, 因此在指派 b[a]的時候, a已經指派為 1所以 b[1]會指派為 4 .

    串接多組標的清單


    指派敘述中開頭的標的清單與等號可以多組串接, 每組標的清單可以不一樣, 例如以下最簡單的範例:
    >>> a = b = c = 1, 2
    
    就相當於是三個指派敘述:
    >>> a = 1, 2
    >>> b = 1, 2
    >>> c = 1, 2
    
    如果需要, 個別的標的清單也可以是不同的結構, 例如:
    >>> a = b, c = d, *e = 1, 2
    
    就相當於以下三個指派敘述:
    >>> a = 1, 2
    >>> b, c = 1, 2
    >>> d, *e = 1, 2
    
    所以最後的指派結果如下:
    >>> a
    (1, 2)
    >>> b
    1
    >>> c
    2
    >>> d
    1
    >>> e
    [2]
    >>>
    
    要特別注意的是雖然可以把串接看成是多個指派敘述, 但最右邊等號後面的運算式只會執行一次, 例如:
    >>> a = [1, 2, 3]
    >>> b = c = a.pop()
    >>> b
    3
    >>> c
    3
    >>> a
    [1, 2]
    >>>
    
    由於 a.pop()只會執行一次, 得到 a串列尾端的 3所以 b Eye c都被指派成 3而 a的內容是彈出 3之後的 [1, 2] .

    使用 _ 耗掉不想要的項目


    如果等號右邊的物件內含一些後續不會用到的項目, 為了符合語法的規定, 就必須在標的清單中列出對等數量的標的, 例如:
    >>> a, b, c = 1, 2, 3
    
    其中若後續的程式不會用到 b閱讀程式時可能會很疑惑他們到底用在哪裡?在這種情況下, 慣例上會用特別的名稱 c來取代 _ Eye b :
    >>> a, _, _ = 1, 2, 3
    >>> a
    1
    >>> _
    3
    >>>
    
    c其實並不特別, 是合於 Python識別字語法規定的名稱, 只是它不具一般人認知的語義, 所以慣例上就用它來綁定語法上需要、但是程式不會再用到的物件.由於它就是個正常的名稱, 所以也可以加上星號, 例如:
    >>> a, *_ = 1, 2, 3
    >>> a
    1
    >>> _
    [2, 3]
    >>>
    
    如此, 就可以用 _來耗掉我們不需要的物件.

    畝運算器


    如果你真的想要像是 C 語言那樣指派完後直接加入運算式運算, 可以使用 Python 3.8才新增的 := 運算器例如:
    >>> a = (b := 4) + 1
    >>> a
    5
    >>> b
    4
    >>>
    
    但是運算器左邊一定要是單一個識別名稱, 而不能是容器內的項目, 例如以下會出錯:
    >>> b = [1, 2]
    >>> a = (b[1] := 5)
      File "<stdin>", line 1
        a = (b[1] := 5)
             ^^^^
    SyntaxError: cannot use assignment expressions with subscript
    >>>
    
    運算器右邊則必須是一個運算式.

    增強型設定敘述 (拡張代入文)


    你可能已經想到, 既然 :=不是運算器, 那麼 = Eye +=等等這一些一定也不是運算器, 沒錯, 他們都是所謂お得な情報語法的組成份子.指派的標的只能是識別名稱、物件的屬性、容器內的索引項目或是切片, 例如:
    >>> a = 10
    >>> a += 10
    >>> a
    20
    
    它會先計算右側的運算式, 再以指派的標的及右側運算式的運算結果當運算元進行指定的運算, 再將運算結果指派回標的.如果標的是切片, 就會置換切片位置的內容:
    >>> a = [1, 2, 3]
    >>> a[1:] += 4, 5
    >>> a
    [1, 2, 3, 4, 5]
    >>>
    

    增強型指派敘述 (拡張代入文) 小結


    Python的指派敘述看似簡單, 但是變化多樣, 而且在許多套件中也會利用各種複雜結構傳回結果, 了解指派敘述的不同型式, 就可以用最簡潔有效的方式取得傳回結果的細部資料, 而不需要使用多列程式拆解內容.舉例來說, -=模組的 time會傳回一個內含日期時間細部資料的可走訪物件, 如果只想取得年份, 可以這樣寫:
    >>> import time
    >>> year, *_ = time.localtime()
    >>> year
    2022
    >>>