Python 功力提升的樂趣Python 功力提升的樂趣

2024/06/30:新增「心得與總結」篇章,本書全系列正式完結。

2024/05/05:重新編輯全文並刪除部分內容,使文章更緊湊。

這是《Python 功力提升的樂趣:寫出乾淨程式碼的最佳實務》閱讀筆記的第 3 篇,你可以把它當作是一則重點整理,加上我個人的開發經驗與心得。

本文整理書中的第 10、11 章,且篇幅幾乎集中在前者。無論什麼語言,「寫好函式」這件事總是如此重要,Python 自然也不例外。

系列:Python 功力提升的樂趣

  1. 使用 Black 格式化程式碼——《Python 功力提升的樂趣》筆記
  2. 如何寫出 Pythonic 程式碼——《Python 功力提升的樂趣》筆記
  3. Docstring 的重要性——《Python 功力提升的樂趣》筆記
  4. 《Python 功力提升的樂趣》心得:Python 開發 Clean Code 入門指南

第 10 章:寫出有效率的函式

有效率的函式(或說「好的」函式)需要你在「命名、規模大小(行數)、參數數量和複雜性」之間,做出許多決定和取捨

本章探討的正是這些取捨之間的利弊得失,以及編寫函式的重要原則。

不用說,這是關鍵的一章。


函式的規模:小就是好?

我們都聽過「函式應該盡可能簡單、一次只做一次件事」之類的建言,也表示認同。從這個精神出發,太大或太複雜的函式就應該要進行拆分。

但!事實是,有效拆分函式是一件耗神、講究細節,且沒有標準答案的事。以致於我們即使知道,也很難完全貫徹,包括我自己。

出於各種原因,我們常常對現實世界作出一定的妥協。

小函式的優點

有些人覺得任何函式都不應該超過 20 行,甚至 10、5 行😂。因為函式「短」往往有下面這些優點:

  • 單一函式容易理解
  • 較少的參數(這確實非常重要!)
  • 易於測試與除錯

小函式的缺點

但小也有缺點:

  • 一樣的邏輯,更小的函式也意味著「更多」的小函式
  • 函式愈多,結構就愈複雜。即「函式間」的關係會變得更加複雜
  • 愈多函式,函式間的精準命名將成為巨大的挑戰——這真的很困難!

這些「缺點」往往也解釋了為何我們不一定那麼積極拆分函式,讓每一個函式都符合「一次只做一件事」原則。

尤其是小函式造成的大量命名問題,對於命名很講究的我而言,有時確實感到棘手。

小結:小不等於短

函式原則上還是應該要盡可能單純一點,該拆就要拆,但不一定要很短。而且其中必然會有很多挑戰。

從「功能」上去劃分界限、拆分函式,會更有意義與指導性,與可行性。

作者認為,一味追求短函式,確實可以讓各別函式變得簡單,但卻很可能讓程式的「整體」變得複雜,適得其反。

他的經驗是,理想情況下,函式最好少於 30 行,最多不超過 200 行。讓函式在合理情況下盡可能短少,但不只是為了短少而縮減。


return 應該要有相同的資料型別

對此,我想說:

這真的好重要啊!(吶喊)

卻常常沒有被好好遵守。

簡言之,為確保函式的「可預測性」,我們應該努力讓函式只回傳「單一資料型別」的值。比如總是回傳整數或字串,而不要有時回傳字串,有時則回傳布林值。

這不一定容易做到,但我更常遇到的情況是:明明有替代方案讓回傳型別單一化,卻沒有善用。

False代替raise

最常見的例子就是:該拋出錯誤時候,卻只用return False 替代

意即,當函式正常執行時,回傳一般正常的 output 值。但當執行失敗時,卻是回傳False——這簡直令人髮指,而且我相信你一定看過這樣的函式。

必須說明,發生錯誤時不拋出而選擇return False未必總是不好的,就像 Django REST framework 序列化器的is_valid方法,預設也是返回一個布林值(可以用raise_exception=True參數改為直接拋出錯誤),方便你進行更多後續操作。

不過,如果你選擇在遇到錯誤時return False,則應該在函式正確執行時,return True以保持回傳型別的一致性。

而且函式命名也要跟著配合,讓人一看就知曉該函式、方法會返回一個布林值,比如上述的is_valid,或常見的has_permissionis_authenticated等。這些都是常見的最佳實踐。

錯誤示範

我們看一下這個錯誤示範:假設我們有一個函式,目的是從一個 JSON 文件中讀取設定資訊。

如果讀取成功,它會返回一個 Python 字典;如果讀取失敗,它會捕捉異常——並返回False

1
2
3
4
5
6
7
8
9
import json

def get_config_from_file(file_path):
try:
with open(file_path, 'r') as f:
config = json.load(f)
return config
except Exception as e:
return False # 這裡是問題所在

這個函式在except區塊裡面直接返回False,這會導致以下問題:

  1. 資料型別不一致:正常情況下返回一個字典,異常情況下返回False(布林值)。
  2. 誤導函式使用者:使用者可能會誤以為False是一個有效的設定(只是設定值為False),進而嘗試在其上進行操作,導致更多的錯誤。
  3. 後續處理困難:函式使用者必須額外檢查返回值是否為False,然後再決定是否進行後續的操作。

return型別不確定」不只發生在例外處理,但它確實很常發生在例外處理,我看過不止一次類似的真實案例。

除此之外,還有「函式正常執行回傳一個類別物件,失敗時回傳一個 Python tuple——包含錯誤代碼和錯誤訊息」這種非常反直覺的設計。彷彿是在告訴我們:

這個函式的回傳值,可能是一個物件,也可能是一個 tuple,你自己判斷吧!

會寫出這樣的函式,原因諸多——工作很忙、重構太麻煩了,要新增什麼功能我直接「加上去」就好!

這些無疑都是充滿了「技術債味」的做法。

技術債與認知負擔

這類「雙型別 return」函式,對於函式使用者(比如你的同事)的認知與理解,有著「更高的要求」——呼叫方必須很了解這個函式的怪異行為,才能正確使用與處理後續衍生的問題。

這在多行或有多個 return 值的複雜函式時,真是一場災難。

期望他人知道自己做了什麼「特別的事」,不是我所知曉的軟體開發之道。

當函式具有這種「雙型別 return」的特性時,會明顯增加呼叫方的「認知負擔」。

這使得程式不僅難以閱讀和維護,也容易出錯,因為未來的維護者或其他團隊成員很可能不知道這個函式的「獨特」行為。

無論何時,我們都不應該寫這樣的程式。


我對寫好函式的基本看法

寫好函式的重點實在太多了,而本文的篇幅有限,只能擇要為之。

我也講講我認為函式的撰寫中,最重要的兩點。

至少遵守這兩點,你的同事會很感激你。

一、Docstring 真的很重要

盡量寫 docstring,儘管這真的不容易,畢竟維護 docstring 也需要心力。

Docstring 就跟所有開發文件一樣——自己很懶得寫,但如果我想調用別人寫好的程式時,卻希望它們越詳細越好。

而且 docstring 也不是有寫就行,還需要從「讀者(也就是你的同事)」的角度去思考與表達。不然看起來會很像開發者的自言自語——沒人看得懂。

何時建議寫 docstring?

我承認,要為「所有」的模組、類別、函式寫 docstring,未免有點不切實際。在眾多函式中,下列兩種是我認為一定要寫 docstring 的:

  1. 專案「自定義」成份濃厚:除了開發者本人,沒人知道這段程式在幹什麼。這通常源於特殊的業務需求,而且往往行數超多、邏輯超手刻,各種 if/else、for 迴圈滿天飛,aka——沒人想看的程式碼
  2. 流程相對複雜的函式:愈複雜就愈難理解,這時候 docstring 就是你的好朋友。用文字描述函式的輸入、輸出、邏輯,能大大提升我們理解程式碼的效率。

畢竟,看有描述性的文字,總比看一長串程式碼,要簡單且友善得多。

上述兩種情況,若不寫 docstring,那麼閱讀程式碼的成本就會大大提升。這相當於在告訴你的同事:

我離開公司後,誰還想維護這段程式碼,先學習通靈術吧!

關於我對 docstring 的其它討論,可參考下面內容:

我的觀點一向如此:不寫好 docstring,就稱不上是一流的 Python 開發者。


二、函式的行為與命名要一致

其二,好的函式要「言行一致」。

你可能會想:

這不是理所當然的嗎?

對,它「本應該」是理所當然的,畢竟這不就是函式命名的基本目的?——用來描述函式的行為。

但我們可以回想一下自己在工作中遇到的各式各樣函式,究竟有多少比例,是真正做到「言行一致」?我覺得可能只有一半。

或許你會認為「一半」也太誇張了!但我並不這麼想。

「言行不一致」通常有下面幾種症狀:

  1. 函式名稱只表達了函式「部分」的行為。也就是函式做了超過它宣稱要做的事,比如「驗證欄位」函式,竟然還把驗證資料格式化了!
  2. 函式名稱「言過其實」,說要驗證加格式化,結果只做了一半。
  3. 名稱太模糊、缺乏業務邏輯描述、濫用技術詞彙等等,根本看不懂它在說什麼,更別說言行一致了。

如果你不能從一個函式的名稱中有效理解並推測它應有的行為,那麼這個函式基本上就是失敗(或不健康)的。

很多時候,函式最初可能是「言行一致」的,但隨著後來的修改、刪除、擴充,實際上做的事情變更了,但命名卻沒有跟著改變、重構。

這些言行不一的函式,充滿誤導性,不斷地挑戰著你的認知、推理能力,更增加了維護成本。

這樣的例子還少嗎?恐怕每天都在發生。


第 11 章:注釋、docstring 和 type hints

這章我只摘錄書中的一段話——我特別欣賞與認同的部分:

好的注釋對程式設計師在未來閱讀並理解程式碼作用時提供了簡潔、有用和準確的資訊。這些注釋應該用來解說程式設計師原本的意圖,並總結某程式碼的作用,而不是只對某行程式碼進行解說。

注釋有時會詳細描述程式設計師在編寫程式碼時所得到的經驗教訓,這些寶貴的資訊可以讓將來的維護者不必再次經歷這些苦難。

說的太好了!

團隊寫程式,是關於溝通的藝術,畢竟《人月神話》已經告訴我們:人多不一定比較快。

溝通不止發生在會議、Jira、Slack 和規格文件上,程式之內也有著大量的溝通,註解是如此,docstring 亦是如此。

永遠不要低估「對這些細節的用心」所帶能來的巨大影響力。

優秀的工程師絕不可能輕忽它們。