Python 工匠Python 工匠

這是《Python 工匠|案例、技巧與開發實戰》筆記的第 1 篇,你可以把它當作是一則重點整理,加上我個人的開發經驗與心得。

從書名推敲,我們並不容易知道本書的主題為何。事實上,和《Python 功力提升的樂趣》類似,這是一本關於「Clean Code in Python」的書。

而且我認為它的難度適中(好吧,後半部難度比較高,而且本書「不適合」初學者),非常推薦看完《Python 功力提升的樂趣》後,想要更進一步寫出 Pythonic 程式碼的讀者與開發人員。

我覺得,兩本恰恰都是屬於「從書名看很容易被忽略」的好書。因此,作為喜歡本書的讀者,我覺得自己有義務,向你們轉述書中一些值得傳誦的內容。

這也是我寫「閱讀筆記」類文章的核心精神——分享書中那些我覺得特別精彩、贊同的部分,並加上自己的看法。

系列:Python 工匠

  1. 《Python 工匠》筆記(一)如何寫好註解
  2. 《Python 工匠》筆記(二)對「單元測試」的看法與建議

本文目錄

  1. 如何寫好 Python 註解
  2. Python 註解基礎知識
  3. 新手常犯的三種註解錯誤
  4. 一、直接註解程式碼
  5. 二、僅用註解「重述」程式碼行為
  6. 指引型註解
  7. 提煉為獨立函式
  8. 三、弄錯註解的「受眾」
  9. 自言自語的註解
  10. Docstring 與讀者意識
  11. 好的程式碼到底需不需要註解?
  12. 作者的觀點與我的看法

如何寫好 Python 註解

本文整理第一章的其中一部分——關於「如何寫好註解」的討論。

之所以要特別寫成筆記,是因為這是我目前看過的書中,討論註解時講最得好的一本。尤其是一些使用上的建議,和我的開發經驗與價值觀可謂非常吻合!

本書作者朱雷(piglei),擁有超過 10 年的 Python 開發經驗,精通 Python 語言特性,對如何開發高品質的大型 Python 專案有獨到見解。

其實我一直也想整理一篇關於寫好註解的基本守則,但遲遲沒有行動。但現在有這本書,我只要整理書中的內容,並加上自己的看法即可。

有關 Python 註解或 docstring 的討論,我在過去多篇文章中都有提到,可參考如下:

開始正文。


Python 註解基礎知識

Python 中,一般我們講到註解,指的是程式碼中的註解,用#來實現。

docstring 則是另一種更具有 Python 特色的註解。主要寫在模組、類別、與函式的開頭,並透過物件的__doc__屬性,自然地化為程式碼的一部分。

書中提到了 docstring 的幾種常見風格(畢竟它本質只是一堆字串,所以怎麼寫都行),最常見的為 Sphinx 文件風格。而我個人在工作上最常用的,則是 Google 風格

簡言之,下面提到「註解」二字時,對上述兩種 Python 註解都適用。

新手常犯的三種註解錯誤

你可能聽過「很多註解都是爛註解」這種說法。不得不承認,這相當程度是對的!我確實看過很多爛註解——但這不是我們因此不寫註解的理由。

實務上會有很多爛註解,正是因為我們沒有正視註解的價值,認真學習如何寫好註解。

因此,就讓我們用書中所舉三種常見的註解錯誤,作為學習的切入點。

書中提到的這三點——尤其是第 3 點,應盡可能避免。作者也提出了相應的解決之道,而我會適時補充我的看法。

附帶一提,本書是從作者過去的網路文章整理、出版——但內容增加了很多。而且作者也很大方,在網路上公開了部分的內容。而本文整理的部分恰恰是公開的部分。有興趣的話可以直接參考本頁

當然,我還是強烈建議,為自己入手一本,你絕對不會後悔。


一、直接註解程式碼

把已經寫完但暫不需要(以後是否需要還不確定)的程式碼,先註解起來,方便日後需要時可以快速「還原」,絕對是我們非常熟悉的手段。

我回想一下,不得不說,這類被註解的程式碼,十之八九都是不會再用到了!如果這類「程式碼註解」愈積愈多,真的會讓人看了很「阿雜」!

所以基本上,現在比較正規的做法,都是建議你直接刪除,以後真的需要時,再透過版控回復即可。

對此我基本認同,所以工作上 code review 時,我「不會」放行這種直接註解的程式碼。

僅有的例外

但,你我都知道,有時事情也沒那麼簡單。

用 Git 版控回復的前提是:你的團隊 commit 記錄要寫好!不然真的要「回復」的時候,你可能連它在哪一個 commit 都要找好一陣子。

所以,基於方便與實際考量,事實上我還是有一點點折衷:原則上不可以註解程式碼,但如果確定只是「暫時」用不到,等別的元件完成後,就會繼續開發、使用,例外可以暫時註解就好——但必須加上 codetag 標記。

一般我們用 TODO 這個 codetag。比如:

1
2
3
# TODO 日後改回一對一時,請用下面方式重寫:
# user = User.objects.select_related('oversee_tenant')
# .prefetch_related('oversee_projects').get(user_uuid=user_uuid)

不過大原則還是:

別註解了,刪除吧!


二、僅用註解「重述」程式碼行為

這應該是我們最常看到的爛註解的形式,也是你在所有討論程式碼註解的書中,一定會提及的。

而且它真的爛,沒有藉口。比如這樣:

1
2
3
4
5
6
7
# 初始化 x 為 0
x = 0

# 檢查 x 是否小於 10
if x < 10:
# 印出 x 小於 10
print("x is less than 10")

這當然是一個誇張的例子🤣。但在日常開發中,這種「脫褲子放屁」的註解還真的不算少見。

確實,如果我常常看到這樣的註解,恐怕真的會忍不住說出「你還別寫註解了吧!」

但,請不要放棄治療!

想避免這個問題,請遵守一個簡單且常見的大原則,如書中所言:

應該儘量提供那些讀者無法從程式碼裡讀出來的資訊。描述程式為什麼要這麼做,而不是簡單複述程式碼本身。

不過,光寫「為什麼」註解,有時候還是遠遠不夠的。

指引型註解

因此,本書更進一步提出所謂的「指引性註解」:

這種註解並不「直接」複述程式,而是簡明扼地概括程式碼功能,起到「程式碼導讀」的效果。

書中的例子:

1
2
3
4
5
6
7
8
9
10
# 初始化存取服務的 client 物件
token = token_service.get_token()
service_client = ServiceClient(token=token)
service_client.ready()

# 呼叫服務取得資料,然後進行過濾
data = service_client.fetch_full_data()
for item in data:
if item.value > SOME_VALUE:
...

不難看出,這種描述「程式碼流程大綱」的指引型註解,很像 docstring 在做的事。從這個角度,我們可以說 docstring 也是一種指引型註解。

但是,docstring 畢竟只出現在元件的「開頭」,對複雜的程式而言,在元件的內部,往往也需要這樣的註解,作為閱讀複雜程式碼的指引。

要寫好指引型註解,甚至知曉「什麼時候」應該要寫下指引型註解,需要你內心清楚——「這段程式碼的哪些部分,別人可能會看不懂!」

這是為什麼我總說,要寫好註解,就一定要培養好「讀者意識」的理由。

指引型註解的價值

前面提到,寫註解時「應該儘量提供那些讀者無法從程式碼裡讀出來的資訊」,這是我們寫註解的大原則。

但指引性註解和上述註解所有區別,它的獨特價值,如書中所言:

指引性註解並不提供程式碼裡讀不到的東西——如果沒有註解,耐心讀完所有程式碼,你也能知道程式做了什麼事。指引性註解的主要作用是降低程式碼的認知成本,讓我們能更容易理解程式碼的意圖

說得非常好。

提煉為獨立函式

對於複雜而冗長的程式流程,書寫「指引性註解」是一個協助閱讀理解的好方法。

而另一個有效的方法,就是把這些程式碼片段獨立成一個又一個的函式,透過有意義的函式名稱來描述流程、展現意圖,此時就可以刪除指引性註解:

1
2
service_client = make_client()
data = fetch_and_filter(service_client)

其中的重點在於,你要能判斷,什麼時候該寫「指引性註解」,而什麼時候則適合獨立成函式、方法。

這當然需要經驗累積,但我們心中要先有這樣的意識,不是嗎?


三、弄錯註解的「受眾」

本書這段主要適用的是 docstring,不過我覺得「指引型註解」也有類似議題,所以本段提到「註解」一詞時,皆包括這兩者。

註解的正確受眾,或說讀者,應該是誰?我想基本上是這兩種人:

  1. 未來的自己(注意「未來」二字)
  2. 專案協作者(同事、主管)

我們先看看書中所舉的「反例」:(我直接引用網頁上的內容,所以技術名詞並非台灣用語,還請見諒)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def resize_image(image, size):
"""將圖片縮放為指定尺寸,並返回新的圖片。

該函數將使用 Pilot 模塊讀取文件對象,然後調用 .resize() 方法將其縮放為指定尺寸。

但由於 Pilot 模塊自身限制,這個函數不能很好的處理尺寸過大的文件,當文件大小
超過 5MB 時,resize() 方法的性能就會因為內存分配問題急劇下降,詳見 Pilot 模塊的
Issue #007。因此,對於超過 5MB 的圖片文件,請使用 resize_big_image() 替代,後者
基於 Pillow 模塊開發,很好的解決了內存分配問題,性能更好。

:param image: 圖片文件對象
:param size: 包含寬高的元組:(width, height)
:return: 新圖片對象
"""

這個例子的最大問題,是寫了太多「實作細節」,講白了就是提供了「太多」程式碼的讀者並不關心的內容!

為什麼這樣是不妥的?細節不是很好嗎?

對,但大部分時候,docstring 或「指引型註解」的主要寫作目的,是為了讓讀者不用一行一行閱讀程式碼,就能夠快速知道目前程式碼的流程與意圖

這種過多細節的寫法,恰恰與這個目標背道而馳——增加了太多理解上的「雜訊」。

書中給出的改善版本如下:

1
2
3
4
5
6
7
8
9
def resize_image(image, size):
"""將圖片縮放為指定尺寸,並返回新的圖片。

注意:當文件超過 5MB 時,請使用 resize_big_image()

:param image: 圖片文件對象
:param size: 包含寬高的元組:(width, height)
:return: 新圖片對象
"""

以下主要是我自己的看法。

自言自語的註解

你有沒有一個疑問,為什麼說註解寫了太多細節,是「弄錯受眾」?

我是這樣看的:留下這麼多細節的註解,與其說是註解,更像寫給自己看的筆記——而且是給「現在」的自己,真的就像在作筆記一樣!

我想強調,「現在的自己」並不是註解的受眾!因為現在的自己對程式細節非常清楚,即使沒有註解也能讀懂程式碼。

相反的,註解是為沒時間慢慢讀程式碼的人服務的。

未來的你,再回來看這段程式,絕對不會想要在 docstring 看到這麼多「廢話」。這種寫法無疑是搞錯了對象。

當然,你的同事也不會想看這麼多雜訊——同事往往更關心「這函式到底要怎麼用」!

抽象層次混亂

我覺得還有另一種「弄錯受眾」的註解也很經典,甚至更加常見!那就是混合底層實作與業務邏輯:

註解中參雜了業務邏輯,又不時會出現底層實作的細節描述。

事實上,我倒覺得實務中會願意寫一大串 docstring 的人,可以說是少之又少。大部分的問題其實是:有寫,但寫得「零零落落」。

Docstring 與讀者意識

Docstring 絕對能看出一個人的基本寫作能力,以及是否具備「讀者意識」。

說真的,這不止是作為一個軟體工程師的核心技能——更是任何表達者的核心技能。而程式,只是表達的其中一種形式。

我很想要寫一篇「如何寫好 docstring」,這需要再構思一番。但常見的錯誤不外乎:

  1. 預知能力:知曉「函式以外」的事、不僅知道函式會怎麼、何時被調用,還會在 docstring 中寫下呼叫時的情境細節——你知道的太多了!
  2. 太多底層細節:跟前面的書中內容相似,只是我覺得現實中,往往是東寫一點、西寫一點,不會像寫筆記一般完整——所以讀起來更痛苦,真的是自言自語!除了開發者自己,誰能輕易讀懂那些細節?
  3. 業務邏輯與底層實作的用詞混雜:一下子是「無法取得租戶資訊」一下子卻又是「防止 SQL insert 錯誤、RabbitMQ 如何如何」——讓人大腦很混亂。

總的來說,無論是「自言自語」還是「抽象層次混亂」,它們的本質都差不多——這些註解都像是寫給「現在的自己」看的筆記

但就像前面說的,現在的自己是最不需要看註解的人!所以才說是「弄錯受眾」了。


最後不免俗地,我們要討論,提到「程式碼註解」就一定避不開的問題

到底要不要寫註解

好的程式碼到底需不需要註解?

寫程式到底要不要寫註解,一直有兩派說法,是個老掉牙又爭論不休的問題。我們先來看看這兩派的觀點。( ChatGPT 整理)

寫註解的一派

這派人認為註解是程式碼的一部分,應該寫註解,主要論點為:

  1. 可讀性提升:註解能夠幫助人們更快理解程式碼的意圖和複雜的邏輯,尤其是對於那些不那麼直觀的部分
  2. 溝通工具:註解被視為開發者之間的溝通方式,尤其在團隊協作時,能夠快速傳遞開發者的想法和注意事項。
  3. 提醒與說明:註解可以用來提醒未來可能的問題,或是對程式碼中的決策提供背景說明。

不寫註解的一派

相對的,這派人認為好的程式碼應該是自解釋的,不需要註解,主要論點為:

  1. 程式碼即文件:好的程式碼應該是自解釋的,如果你需要註解來解釋你的程式碼,那麼問題可能出在程式碼本身。
  2. 增加維護難度:註解需要維護,不一致的註解比沒有註解更糟,因為它會導致誤解和混淆。
  3. 過度依賴註解:過多的註解可能會讓開發者過度依賴於它們來理解程式碼,忽視了提高程式碼品質的重要性。

作者的觀點與我的看法

細看這兩派的主張,我們可以看出,都有一定的道理——不然也不會爭論不休了。

作者觀點

我們先來看看本書作者的觀點(引用難免斷章取義,完整上下文請見本書第 15 頁),下面內容主要是回應「不寫註解的一派」:

但我倒是認為事情沒那麼絕對。無論程式碼寫得多好,多麼「自說明」,跟讀程式碼相比,讀註解通常讓人覺得更輕鬆

註解會讓人們覺得親切(尤其當註解是中文時),高品質的指引性註解確實會讓程式碼更易讀。有時抽象一個新函式,不見得就一定比一行註解加上幾行程式碼更好。

第一段引用,其實就跟我在「Python docstring 之我見」中的主張一致:

在我看來,無論程式寫得如何簡潔易讀,對一些比較複雜的函式或類別而言,docstring 終究是不可少的。因為文字的詮釋能力和程式碼相比,絕不在同一個層次,相信這也是為何 docstring 會有屬於自己的獨立 PEP 加以規範的理由。

我的看法與結論

既然都寫了這篇文章,想當然爾我是「支持」寫註解的一派。

不過,反對派說註解會增加維護難度,以及可能讓人更加依賴註解而忽略了提升程式碼品質,我覺得這些擔憂也非常真實

所以,對我來說,問題不在於「要不要寫註解?」,而是「怎麼樣才能寫好註解?」。這也是本文想回答的問題。

我的結論是:寫註解,但也要適度。並保持對「讀者意識」的敏感:不寫多餘的註解,也不寫自言自語的註解。

延伸閱讀