《Effective Python》筆記(一)以拆分代替索引
Effective Python 中文版
這是《Effective Python 中文版|寫出良好 Python 程式的 90 個具體做法》筆記的第 1 篇,你可以當作是一則重點整理,加上我個人的開發經驗與心得。
毫無疑問,這又是一本關於「Clean Code in Python」的書——而且很可能是評價最高的一本。
本書由 90 個具體做法所組成,搭配大量實例,助你寫出更 Pythonic 的程式碼。
內容有初階也有進階(比如 metaclass),我會挑選書中我認為「重要且實用」的部分作為分享準則。簡言之,不會太深——但很可能是常常被忽略或低估的部分。
本文整理自書中的「做法 6:優先選用多重指定的拆分而非索引」。
附帶一提,作者已經把書中的程式碼範例都放在 effectivepython 這個 repo,這對於我這種要寫筆記、心得文章的人,真是再方便不過。
本文主旨
本文的主旨只有一個:試圖說服你,不要在程式碼中使用索引取值——盡可能改用拆分(unpacking)來代替索引。
這裡的索引取值,指的是像items[2]
這樣,以序列(sequence)的數字索引來取值的情況。
如果程式碼只有你一個人撰寫與維護,且日後也是如此,或許未必要堅持這個原則。
但如果涉及多人協作,未來還有交接與維護需求,那麼避免索引取值,肯定能提升程式的可讀性,同時更容易維護。
索引取值的問題
索引取值的根本問題,正是基於上述考量:
- 可讀性差:光憑
items[2]
,我們不知道 2 代表什麼。 - 容易出錯:索引值的表意性弱,容易誤取,這可能導致程式崩潰。
- 維護困難:今天你還記得 2 是什麼意思,半年後就不一定了。
就我的經驗,索引取值很常和 for 迴圈一起使用,因為它們都是爬取資料的常見手段,而且是很「手刻」的那種——即完全不通用。
一般而言,爬取資料的程式碼,對於非實際開發者,往往已經很不容易閱讀。
如果再加上索引取值,容易讓人倍感困惑。
為索引值寫註解,是一個折衷辦法,但依舊不是最好的那個。
拆分(unpacking)
使用拆分(unpacking)才是更好的做法。
拆分是指將 sequence、iterable 中的多個元素,一次性賦值給多個變數的操作。
這個方式能提高可讀性,避免使用神秘的數字索引,讓你的程式碼更加清晰明瞭。
拆分的基本用法如下:
1 | a, b, c = [1, 2, 3] # sequence unpacking |
索引 vs 拆分
拆分相對於索引取值,對可讀性有著明顯的進步。
我們看書中的例子,這是索引取值:
1 | # Example 2 |
這是拆分:
1 | # Example 4 |
拆分顯然更簡潔、更直接。
雖然最後都是賦值給了變數first
和second
,但少了中間的索引取值,程式碼中的「視覺雜訊」也減少了——這很重要。
拆分在協作中的價值
平心而論,索引取值對於撰寫程式的人而言,往往是比較輕鬆的做法,所以我們更偏愛用索引。
但對於閱讀程式碼的人,就有點辛苦了。
推薦使用拆分的目的很簡單——避免透過索引取值再賦值給變數,而是直接賦值給多個變數,減少中間流程,也減少視覺雜訊。
更別說,有些索引取值「並不會」再重新賦值給新變數,這類情況更應該使用拆分。(詳見最後「我的實作」)
明確的意圖
除此之外,拆分還可以讓你的「程式意圖」更加明確!比如:
1 | member_names = [member[1] for member in members] |
這樣的寫法容易讓我有兩個疑問(這是我自己寫過的程式碼):
- 索引 0 是什麼?完全不需要嗎?我可以忽略嗎?
- 索引有 2 或 3 嗎?(即
member
有更多元素嗎?——我們無法確定)
這些不確定因素,讓你的程式意圖變得模糊。雖然自己清楚,但對於程式碼的閱讀者,則是一種不必要的「認知負擔」。
這些認知負擔不斷累積,將導致人們需要花更多時間,才能讀懂你的程式碼。
而拆分的寫法,可以讓情況明確許多:
1 | member_names = [name for _, name in members] |
我們透過拆分與變數大聲宣布:member 總共只有兩個元素,而且我只要第二個!
進階拆分
但我就是只需要其中一個變數,怎麼辦?就像前述的
items[2]
以拆分取代索引,我們需要一些「配套措拖」,才能夠用得方便、滑順。
其中最常用的,就是_
和*
。
善用_
來進行拆分
很簡單,把不要的內容丟給_
變數。
1 | _, _, last = ["apple", "banana", "cherry"] |
這樣寫最大的好處,是明確告訴讀者:我不需要前兩個元素,所以直接丟給_
。
善用*
進行拆分
如果你想要的元素在第一個或最後一個,而且拆分的元素較多,那用*
會更加方便。
使用星號(*
)收集其餘的值:
1 | first, *middle, last = [1, 2, 3, 4, 5] |
其中middle
的變數內容,會是一個擁有 0 到多個元素的list
。
結合_
和*
第一個例子只有 3 個元素,而第二個例子有 5 個元素。
如果有更多,比如 10 個,而我們依舊只需要最後一個,難道要寫一堆_
?
當然不!這時可以結合兩者,重構第一個例子:
1 | *_, last = ["apple", "banana", "cherry"] |
是不是非常簡潔?
我的看法
_
很常用,而*
拆分我比較少用(拆分 3 個以上的情況對我相對少見)。
無論如何,只要有數字索引出現,我們第一時間就要想到「能否用拆分來取代?」——通常可以!
這是一個很好的習慣,相信你的同事會感謝你。
書中範例欣賞
我們看一段書中的程式碼,比較索引取值和拆分(其實還用了enumerate
),兩者的差別究竟能有多大:
1 | snacks = [('bacon', 350), ('donut', 240), ('muffin', 190)] |
不過,不可否認,上面的拆分寫法需要你認真閱讀才能讀懂,因為它有點複雜。
但即使不了解其中的細節,也能輕易看出,後者更加優雅。
而且大部分需要你拆分的情境,都比這個例子單純。例如以下的實作。
我的實作
在剛發表的〈31,打造新版「熱門文章排名」〉一文的專案實作中,我用拆分重構了原有的程式碼(內容已略有不同,以下為早前版本)。
這段程式把從 GA4 整理出的網站流量資料,以一定格式寫入 Markdown 文件中。
舊版,使用索引取值:
1 | rank = 1 |
如前所述,索引取值對於程式撰寫者來說,可能是最直覺的做法,也相對輕鬆。所以我一開始也是這樣寫的,但真的不太好讀XD
row[0]
、row[1]
到底是什麼?不清楚——有認知負擔。
以拆分重構
改用拆分重構,並以_
省略不需要的變數:
1 | rank = 1 |
這個例子,比上述所有例子都更能凸顯拆分的價值。
因為row[0]
、row[1]
最終都沒有再被重新賦值給另一個變數,而是直接寫入文件。
使用索引,你幾乎看不出row[0]
、row[1]
究竟代表什麼——只能靠猜。
改用拆分,我們能知曉被寫入的是title
和path
,而不是神秘的索引取值。
結語
總的來說,我覺得拆分是一個大大被低估卻異常實用的技巧。
善用拆分能讓你的程式碼更加 Pythonic。
從今天開始,在 for 迴圈中,就別再索引取值了吧!
相關文章