適度用 Python 海象運算子提高程式碼可讀性與簡潔
Posted on Apr 23, 2024 in Python 程式設計 - 中階 by Amo Chen ‐ 5 min read
Python 3.8 推出 1 個新的運算子 — Walrus Operator, 又稱海象運算子,其運算符號為 :=
。
Python 社群對海象運算子有一些爭議,主要是:
- 海象這個名稱不夠明白、直覺,無法讓人直接從名稱了解其用途
- 無法向下相容,如果你是套件開發者,用了海象運算子就會有 3.8 以前的相容問題要解決
:=
與=
符號太相似,難以快速識別
但無論其爭議為何,海象運算子只用 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