如何寫出 Pythonic 程式碼——《Python 功力提升的樂趣》筆記
文章目錄
Python 功力提升的樂趣
2024/06/30
:新增「心得與總結」篇章,本書全系列正式完結。
2024/05/05
:重新編輯全文並刪除部分內容,使文章更緊湊。
我們繼續《Python 功力提升的樂趣:寫出乾淨程式碼的最佳實務》閱讀筆記,這是第 2 篇,你可以把它當作是一則重點整理,加上我個人的開發經驗與心得。
系列:Python 功力提升的樂趣
- 使用 Black 格式化程式碼——《Python 功力提升的樂趣》筆記
- 如何寫出 Pythonic 程式碼——《Python 功力提升的樂趣》筆記
- Docstring 的重要性——《Python 功力提升的樂趣》筆記
- 《Python 功力提升的樂趣》心得:Python 開發 Clean Code 入門指南
第 6 章:寫出 Pythonic 風格的程式碼
我們常常聽到 Pythonic,但如果問起它究竟意味著什麼,一時之間可能也不容易回答,畢竟它沒有一個公認的標準或定義。
不過,充利分用 Python 獨有的特性、寫作風格、語法,尤其是善用標準函式庫,不重複造輪子——因為你自己造的輪子往往不會更好。或許是大家都認同的部分。
至於遵守上篇提到的 PEP 8,自然不在話下。如果你的變數命名使用了小駝峰式命名法(lower camel case),那麼在「形式上」就已經不太 Pythonic 了。
話說回來,本書雖然是一本優秀的 Clean Code in Python 入門書,但不知為何,書中的函式命名皆是採用小駝峰,讓我不甚理解。
但我們要知道:好孩子不要學。至於為什麼,不用問,問就是 PEP 8。
使用 enumerate()
而不要用 range()
當我 review 到下面這類 code 時,我會強烈要求必須改用enumerate
重構。
設想你有一個 Python list 如下:
1 | animals = ['cat', 'dog', 'moose'] |
你想同時知道每一個元素是 list 中的第幾個與內容為何,直觀上可能會這樣寫:
1 | 'cat', 'dog', 'moose'] animals = [ |
這類「手刻式邏輯」的寫作風格——要什麼就寫什麼,寫完也不重構——你是否已屢見不鮮?我們來評論一下,這段程式碼有兩個很醜(難讀)的地方。
i + 1
。這寫法顯然是為了讓計數從 1 開始——而不是預設的 0。range(len(animals))
寫法非常不直觀,多層的嵌套式呼叫讓人看了昏昏欲睡。
這兩個地方充滿了「手刻」的僵化感,只是為了最快速達到開發者「當下目的」而強行採用的一種粗糙手段——但顯然不是最好的那個。
雖然兩者都不難理解與推測,但除了寫這段程式的人,誰也無法 100% 確信作者的「意圖」——這是糙 code 的特色,它存在著大量且不必要的誤解空間。
這些不明確的誤解空間會造成別人閱讀本段程式碼時的「思考停頓」,更別說後續滋生 bug 的可能。所幸,它們是有可能被消除的。比如使用以下的enumerate
。
用 enumerate()
重構
一言以蔽之,你只要在for
迴圈內,看到以下這樣的寫作模式,那就是典型的 Python 壞味道:
iterable[index]
迭代任何iterable
同時,又需要知道每一個元素的索引值,這是很常見的需求。
因為太常見,所以 Python 早就幫你準備好了——enumerate
。
同樣的需求,我們應該要這樣寫:
1 | 'cat', 'dog', 'moose'] animals = [ |
start=1
讓你不用再i+1
,也不再需要range
和len
的疊床架屋。
兩種寫法的結果是一樣的,但後者更加善用 Python 的內建特性,且優雅。
善用 Python 的自身的特性寫出優雅而簡潔的程式碼——這就是 Pythonic。
使用 with
陳述式而不要手動 close()
因為你很可能會忘記 close!
with
(context manager,上下文管理器)在資源管理上很有用,比如文件讀寫或資料庫連接等,可以確保即使有錯誤發生,也能正確地關閉、釋放資源。
但對於不熟悉或不常見的物件,則要注意物件是否實作了所謂的「上下文管理協定」。
這個協定要求物件必須實作以下兩個方法:__enter__
和 __exit__
。這樣物件搭配with
使用時,才能正常運作。
以 Pythonic 風格運用字典
- 使用
get()
來處理沒有 key 值而出現KeyError
的情況。(不過有時候缺少 key 是一個警訊,此時不宜透過 get 讓這個警訊被忽略) - 使用
setdefault()
來處理沒有 key 值就要新增該 key 並給定一個預設值的情況。 - 使用
collections.defaultdict
來更細緻地處理預設值。
前兩者你可能已經熟悉,而第 3 種在特定情境真的很好用!以前當資料工程師,進行資料清洗時,我就很喜歡用defaultdict
。
關於defaultdict
的用法與介紹,可直接參考這篇〈collections雜談之一 ——— dict的key值存不存在乾我屁事〉。
上面舉的種種例子,都是常見的 Python 寫作慣例,這些慣例是 Python 在設計功能時就已經詳細考慮到的常見情況。換句話說,它們也往往是最佳實踐。
這樣寫,熟悉 Python 的人會知道,你就是一個「內行人」。
還是那句〈Zen of Python〉中的名言:
任何問題應有一種,且最好只有一種顯而易見的解決方法。(There should be one– and preferably only one –obvious way to do it.)
說到內行人,下一章我們就來看看,內行人還需要知道哪些事情。
第 7 章:程式設計的行話
行話,在本章的情境中,指的是程式設計中的「術語」——包括不限於 Python。
物件、值、型別、識別碼
物件(object)是資料的表示形式,例如數字、字串,或更複雜的資料結構,比如 list 或 set。
所有的物件都具有值、識別碼(id)和資料型別。
這句話很重要。因為我們很常把物件的「值」(比如數字 12、字串"hello"
)視為物件「本身」,但實際上那只能說是物件的一個重要屬性。
而物件的其它兩個重要屬性,就是 id(代表物件在當前的程式執行期間在記憶體中的地址)和型別。
id 屬性的內容可以透過內建的id
函式取得:
1 | id(12345) |
而型別則是透過type()
:
1 | type(12345) |
了解這些細節的重要理由是,每次執行程式時,物件的 id 與型別不會改變,但物件的值是可以變動的!這是可變與不可變物件的基石。
此外,如果兩個物件的 id 相同,那麼它們就是同一個物件。這將有助於你理解關於「物件的參照」與「一、Python 中的變數:是水桶還是標籤?」等議題。
可變與不可變
剛剛提到,物件中只有「值」是可以(有可能)改變的。所以依照物件的值是否可以改變,物件也分為兩大類:
- 可變物件。
- 不可變物件。
這無疑是一大經典議題,但這裡就不展開了。只要知道物件的可變與不可變,對於變數的「參照」有著巨大的影響。有興趣可以再看看「二、使用可變物件作為引數時要小心」部分。
本章還有很多術語的定義介紹,比如陳述式(statement)與表達式(expression)、iterable 與 iterator、參數與引數等等,就請讀者自行閱讀。
第 8 章:常見的 Python 誤解和陷阱
本章介紹基於 Python 特性的一些常見錯誤——我們一定都踩過。在此只提「為什麼這樣不好」以及如何避免這些不佳實作的核心想法。
不要在迴圈中新增或刪除「被迭代物件(比如 list)」中的元素
新增可能會造成無限迴圈(因為迭代不完),而且刪除則很可能會造成索引值與你預期內容產生錯亂的問題。
大原則就是「不要在迴圈中編輯(修改)當前被迭代的可變物件」,如果必須這麼麼做,就為它們建立一個「副本」吧!
一定要用copy.copy()
和copy.deepcopy()
來複製「可變值」
承上,既然要建立複本,就要採用穩妥的方式,而穩妥方式就是使用內建方法。
這又又又是一個「物件參照」的相關議題。下一個也是。
不要使用「可變物件」作為「參數預設值」
這點你可能已經很熟悉了,因為幾乎每一本書都會強調。
我們用下列兩段話(by ChatGPT)來回顧,為什麼不能這麼做:
在Python 中,不應該用可變物件做預設參數(引數),因為這個可變物件在函式第一次被定義的時候就已經「固定」了。(只會建立一次)
以後每次呼叫這個函式但沒有提供這個引數的話,都會共用同一個物件。如果你在函式裡改變了這個物件,那麼下次呼叫時,這個改變就被保留下來了。這會讓程式的行為變得很難預測和控制。
一個簡單的例子
參考程式碼,這個函式正常使用時,希望你帶入一個fruit_list
作為引數——當然,從上面的「二、使用可變物件作為引數時要小心」我們知道,這也是一個不太健康的做法!
關於使用可變物件為引數,我們先不論。下面的呼叫我們都不給第二個引數,讓它只使用參數預設值。
我們預期的行為是:每次給定不同的水果,且不給fruit_list
引數,則產出應該都要是「以該水果為單一元素的」的 list——但事實完全不是如此。
1 | def add_fruit_to_list(fruit, fruit_list=[]): |
當「第二次」呼叫函式時,情況就不是我們要的了:
- banana_list 竟然包含了 apple!
- apple_list 竟然也被改變了!
替代方案
而替代方案想必你也知曉,就是把預設值改設為None
,然後在函式中加上一個判斷,比如下面的if fruit_list is None:
。
1 | def add_fruit_to_list(fruit, fruit_list=None): |
當沒有提供fruit_list
參數(引數)時,if fruit_list is None:
判斷式會成立,判斷式內通常會建立一個全新的可變物件,讓每一次呼叫都會有不同的fruit_list
可變物件,避免前述的「狀態殘留」與意料之外的錯誤。
tuple 中即使只有一個元素,也不要忘記加逗號
這個例子非常經典!因為不少人可能寫了超過一年的 Python,都未必知道這個特性,而在某些「意料之外」的事發生後,才赫然發現這個事實。
好,我承認第一次知道時我也很吃驚,還好我是看書知道的。
照標題的意思,單元素 tuple 應該要長這樣:("cat",)
,而不是這樣("cat")
。
就像《流暢的 Python》中說的,逗點才是 tuple 的本體XD。
這也是為什麼我們在函式中 return 複數值時,不需要為它們加上小括號,只要使用逗點分隔,return 自然就是一個 tuple。
1 | def multiple_values(): |
足見,逗點確實是定義 tuple 的關鍵。
相關文章