適度用 Python 海象運算子提高程式碼可讀性與簡潔

Posted on  Apr 23, 2024  in  Python 程式設計 - 中階  by  Amo Chen  ‐ 5 min read

Python 3.8 推出 1 個新的運算子 — Walrus Operator, 又稱海象運算子,其運算符號為 :=

Python 社群對海象運算子有一些爭議,主要是:

  1. 海象這個名稱不夠明白、直覺,無法讓人直接從名稱了解其用途
  2. 無法向下相容,如果你是套件開發者,用了海象運算子就會有 3.8 以前的相容問題要解決
  3. :== 符號太相似,難以快速識別

但無論其爭議為何,海象運算子只用 1 個新的符號,就能使程式碼變得簡潔之外,還能同時滿足 Python 使用者的習慣,長遠來看其實是利大於弊。

譬如下列程式碼,在使用海象運算子之後,其實只需要 2 行即可:

x = input('> ')
while x:
    print(x)
    x = input('> ')

因此,學會適度使用海象運算子是可以帶來好處的!

本文將介紹海象運算子與幾個適合使用海象運算子的場景。

本文環境

  • Python 3.8 以上

海象運算子 / Walrus operator / :=

海象運算子 := , 原文為 Walrus operator, 之所以稱為海象是因為 := 看起來像是倒在地上的海象的眼晴跟一對長牙。

海象運算子是 Python 3.8 之後推出的運算子(其提案為 PEP 572 - Assignment Expressions ),是除了 = 運算子之外的另一種賦值的方式,主要用途是減少重複程式碼或者重複執行的程式碼,讓 Python 程式碼變得更簡潔、高效。

其語法為:

NAME := expr

p.s. expr 可以是任何 Python 的表達式

海象運算子的作用為先執行完運算子右側的 expr 之後,再將 expr 的值指派給 NAME 變數。

同樣具有賦值的功能, := 相較於 = 而言,有著較多的限制,我們稍後再談這些限制。

使用海象運算子的好例子

理解怎麼使用海象運算子的最好辦法,就是直接看使用前與使用後的對照。

以下是常見用 while 迴圈以固定長度(1024 bytes)讀取檔案的範例:

file = open('text.txt', 'rb')
chunk = file.read(1024)
while chunk:
    print(chunk)
    chunk = file.read(1024)

上述範例可以看到我們打開檔案之後,需要先以一行 chunk = file.read(1024) 讀取部分內容,接著才能進入 while 迴圈, while 迴圈會判斷 chunk 是否有內容,才能執行迴圈內的程式,在迴圈結束之前,我們需要再讀取一次檔案,並將內容放到 chunk 內,下一次迴圈才有辦法繼續執行。

如此簡單的範例,暴露 Python 語法層面的不足,導致有重複程式碼的問題產生,相同的程式碼 chunk = file.read(1024) 出現了 2 次。

但如果能夠將前述範例改為類似以下的形式,程式碼將會更簡潔:

file = open('text.txt', 'rb')
while chunk = file.read(1024) and chunk:
    print(chunk)

可惜的是, Python 不支援上述寫法,上述寫法會拋出語法錯誤:

    while chunk = file.read(1024) and chunk:
                ^
SyntaxError: invalid syntax

Python 3.8 之後,藉由 := 海象運算子的幫助,使得前述比較好的寫法能夠實現,以下是改良後的寫法:

file = open('text.txt', 'rb')
while chunk := file.read(1024):
    print(chunk)

看起來是否更簡潔了!這就是海象運算子帶來的好處!

再多看 1 個使用海象運算子的前後對照,抓一下感覺。

海象運算子使用前:

x = input('> ')
while x:
    print(x)
    x = input('> ')

海象運算子使用後:

while x := input('> '):
    print(x)

PEP 572 所觀察到的範例(田野調查實錄)

PEP 572 提到 Guido 在 Dropbox 的 code base 有觀察到 Python 程式碼存在一些現象,譬如 Python 使用者寫喜歡寫更少的行數,而非較短的多行程式碼。

與其寫出正確高效的程式:

match = re.match(data)
group = match.group(1) if match else None

Python 使用者更可能喜歡:

group = re.match(data).group(1) if re.match(data) else None

然而,上述程式碼卻會讓程式變慢,因為它重複執行 re.match(data) 2 次。

使用海象運算子之後,將可以避免重複執行的情況,同時滿足 Python 使用者喜歡用 1 行解決的需求:

group = m.group(1) if m := re.match(data) else None

另外, Python 使用者也會盡可能地減少縮排的情況,譬如下列程式,該範例中使用 2 個正規表示式,用以比對(match) 2 種 pattern 的其中 1 種:

match1 = pattern1.match(data)
if match1:
    result = match1.group(1)
else:
    match2 = pattern2.match(data)
    if match2:
        result = match2.group(1)
    else:
        result = None

為了減少縮排, Python 使用者更可能傾向於使用下列寫法:

match1 = pattern1.match(data)
match2 = pattern2.match(data)
if match1:
    result = match1.group(1)
elif match2:
    result = match2.group(1)
else:
    result = None

上述範例使得所有 data 都會經過 2 次正規表示式比對(match),而原始版本在最佳情況可以只執行 1 次正規表示式的比對。

使用海象運算子之後,同樣可以滿足減少縮排情況,並維持程式碼的效率:

if match1 := pattern1.match(data):
    result = match1.group(1)
elif match2 := pattern2.match(data):
    result = match2.group(1)
else:
    result = None

海象運算子運用在 List Comprehension 的範例

PEP 572 所提到的 Python 使用者喜歡用 1 行或很少的行數解決問題,因此可能導致程式效率下降,這種情況在 List Comprehension 也很常見,下列範例即是如此,下列範例使用 list comprehension 過濾不符格式的年月份字串,並且將年月份的字串以 - 符號分隔之後,轉成 list of tuples 儲存:

raw_data = ['2024-01', 'unknown', '2024-02', '2024-03']

year_mon_tuples = [
    tuple(s.split('-'))
    for s in raw_data
    if len(s.split('-')) == 2
]

上述範例在 if len(s.split('-')) == 2 檢查格式時,已經先分割字串過 1 次,但在轉成 tuple 時, tuple(s.split('-')) 又分割 1 次字串,造成效率問題,但 list comprehension 真的太香了,你又怎能抗拒這種寫法呢?

同樣的需求,轉成使用海象運算子之後,上述效率問題就解決囉:

raw_data = ['2024-01', 'unknown', '2024-02', '2024-03']

year_mon_tuples = [
    tuple(r)
    for s in raw_data
    if len(r := s.split('-')) == 2
]

上述的重點在於 len(r := s.split('-')) 其實等同於下列寫法:

r = s.split('-')
len(r)

解放 List Comprehension 更多可能性

有了海象運算子之後,不僅能夠改善為了可讀性犧牲效率、為了用更少行數完成功能而犧牲效率等問題,其實也為 list comprehension 帶來更多可能性。

譬如下列程式碼是產生 0 到 9 的累計和(cumulative sum) list 的程式,累計和結果會是:

[
  0,  # 0
  1,  # 0 + 1
  3,  # 0 + 1 + 2
  6,  # 0 + 1 + 2 + 3
  ... # 以此類推
]

我們可能會用以下 Python 程式碼產生 0 到 9 的累計和:

cumulative_sum = []
for x in range(0, 10):
    if not cumulative_sum:
        cumulative_sum.append(x)
    else:
        cumulative_sum.append(cumulative_sum[-1] + x)

但是藉由海象運算子,我們可以將上述程式轉變為 list comprehension 形式,盡情享受 Python 的優美:

sum = 0
cumulative_sum = [sum := sum + x for x in range(0, 10)]

可以說海象運算子解放了 list comprehension 更多可能性!

海象運算子的限制與問題

海象運算子雖然方便,但仍有些限制,如果不注意的話,就會出現 SyntaxError

海象運算子 := 不能直接當 =

記住, Python 的 := 不能像 Go 一樣直接當作 = 使用,以下範例會直接拋出 SyntaxError :

x = 0
x := x + 1

錯誤:

    x := x + 1
      ^
SyntaxError: invalid syntax

如果真的一定要可以運作,就只能用小括號將 x := x + 1 包起來:

x = 0
(x := x + 1)

但是真心不建議這樣使用,因為很違反直覺,容易造成可讀性下降。

海象運算子使用時,最好加上小括號

前面說過海象運算子右側的 expr 可以是任何 Python 的表達式,所以它會忠實地從左到右執行,再對最後的結果進行賦值。

例如,下列程式碼 sum 列印出來的值會是 True ,因為 Python 先執行 2 + 2 > 3 得到值為 True ,再將 True 賦值給變數 sum

if sum := 2 + 2 > 3:
   print(sum)

但按照程式碼的原意其實正確的寫法如下, sum 列印出來的值是 4 才對:

if (sum := 2 + 2) > 3:
    print(sum)

這就是為什麼使用海象運算子最好加上小括號的原因,我們最好讓 Python 明確知道海象運算子右側的 expr 到哪裡為止,而不是讓它一路從左執行到右。

總結

其實撇除爭議認真體驗 walus operator 所要解決的問題的話,是可以感受到 walrus operator 所帶來的好處。

既然是真的有用,那其實適度使用也無妨啦!

以上!

Enjoy!

References

PEP 572 – Assignment Expressions

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!