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

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

系列:Python 功力提升的樂趣

  1. 《Python 功力提升的樂趣》筆記(一)Black、命名、壞味道
  2. 《Python 功力提升的樂趣》筆記(二)Pythonic、行話、陷阱

系列緣起

我會用數篇文章,把本書的重點一一勾勒出來。之所以願意這麼做,主要有兩個原因。

要真正落實 Clean Code,著實不易

雖然任何一種程式語言都能夠寫出堪稱簡潔的程式碼,但 Python 可能是最接近「自然語言」的那個,從這個角度看,Python 的 Clean Code 或許是最美的。

但因為 Python 的自由度,往往實際上的程式碼品質都不是那麼理想——你甚至希望它不要那麼自由。

我認為這本書被低估了

意思是它似乎不那麼有名。

當然,講 Clean Code Python 的書也不只這本,但我認為本書是最適合起手的一本。儘管它面向的是 Python 新鮮人,但不得不承認,即使已經寫了一、兩年 Python 的開發者,對於書中建議的落實,往往都還有所不足。

儘管 Python 是自由的,但 Clean Code 卻沒有那麼自由——我們需要這本書,而且要好好實踐它。

只講重點

雖然是偏筆記的形式,但我也不想重複那些基本知識,所以只講重點,視需要給予書中的細節。

重要的是,我們「知道」有這件事。這是一種「嗅覺」,你也可以說,是對於「程式碼壞味道」的嗅覺。

儘管在不久的將來,我們產出的程式碼,可能有相當部分是 AI 幫我們寫的,但那依舊不妨礙我們對於 Clean Code 的追求——畢竟你還是要審視 AI 的產出,並適時修正與完善。


本文整理書中第 3-5 章重點,前 2 章我覺得重要性普通就略過了。

第 3 章:使用 Black 進行程式碼格式化

Formatter——格式化器——想必大家都很熟悉了,我也有不少文章提過(雖然主要講 yapf),有興趣可以查看「Code Formatting」這個標籤。

提 formatter 肯定要提 PEP 8,畢竟所有的 Python formatter 都是以 PEP 8 為基礎,再加上一些慣例或額外規則。

所以這章有一定的篇幅與範例都是在講 PEP 8,這裡只提一個我覺得比較容易被忽略(但我認為重要)的點——垂直間距(空行)。

至於要用哪個格式化器,本書推薦 Black。我認為 yapf 也不錯。

垂直間距

這名稱好學術,講白了就是程式碼之間的「空行」。

空行是一種藝術。

就跟文章的分段一樣,程式碼的空行太多會流於鬆散,而太少則過度緊迫。如何拿捏,需要你對美感與程式可讀性有幾分「直覺」才行。

所幸,PEP 8 有基本規則:

  1. 函式之間:2 行空行。
  2. 類別之間:2 行空行。
  3. 類別的方法之間:1 行空行。

這 3 種空行,格式化器能幫你自動完成。另外,空 3 行或在上述情境「以外」的地方空 2 行,都是不允許的😷

格式化器會一併修正(linter 也會提醒),不會讓你自由發揮。

這麼說來,真正難的,還是什麼時候要空 1 行。我想這就是空行藝術的所在了。

Why Black?

本書推薦 Black 的理由,就跟許多大型 Python 開源專案採用 Black 的理由一樣——因為它的規則最硬,大家只能乖乖遵守。書中是這麼說的:

如果您正在與其他人一起進行某個程式專案,只要使用 Black 工具就能立即解決所有關於格式化程式碼的許多爭論與協調。

真的,只要協作的人愈多,我們對程式碼「一致性」的需求就愈高,優先權甚至高過了典型的慣例與規則。比如 Google 要求內部 Python 專案中的程式碼縮排為 2 個空格而不是常見的 4 個。

2 空格的縮排既不符合 PEP 8,也並非常見慣例,是基於團隊與專案的一致性要求

而一致性的本質就是——這沒得商量

沒得商量,就沒必要爭執,而且誰也不能怪誰,因為這是 Black 規定的!

不過,如同我之前的文章介紹,大多數時候我還是會變更下列兩個 Black 預設值:

  1. 單行字元上限。
  2. 停用強制雙引號。

「一致性」有在團隊協作中多重要?書上的這段話,無疑是很好的註解:

Python 的語法在風格樣式上更有彈性。如果您編寫的程式別人看不到,那怎麼樣寫都可以,但是軟體開發工作大都是共同協作的,無論與其他人一起在專案上工作,或是請經驗豐富的開發專家來審查您完成的程式,把程式碼格式化為大家公認的風格樣式是很重要的。

可不是嗎?


第 4 章:選用易懂的命名

命名之難,是因為你總是要用很心才能命出好名字。而且情境千變萬化,命名有原則,卻沒有太多捷徑。

檢查風格樣式,機器可以代勞,但命名就不行了——那是以前!

現在有了 AI,只要你願意讓它看看你的程式碼,要求它「請幫我找出不恰當的命名」並接受它的建言,我相信肯定很有幫助。

雖然我們通常不想面對批評——哪怕是來自 AI 的批評。我懂,這是人之常情。包括我自己,也會覺得有點難受。

但你的 code 會因此變得更 robust,這是值得的。

兩種不佳的命名模式

一、prefix,命名的前綴

為類別中的屬性命名,重複類別名稱作為前綴,比如在 Cat 類別中,屬性 weight 又命名為 cat_weight。這樣做是冗餘的。

先別說你不會XD,很多時候人都是健忘的。尤其當這個類別是 ORM 中的 Model 時,特別容易!

而且冗餘的前綴不一定只發生在類別,有時模組中的變數命名,也會再度重複模組名稱作為前綴。

二、在名稱中以「循序數字」當作後綴

比如payment1payment2payment3

我真的很討厭!因為這樣的命名缺乏表意性

有時候我會在 pytest 的 fixtrue 看到同事這樣寫,我會建議改成比較「能表現個體特質」的命名。

比如payment1改成payment_with_credit_cardpayment2改成invalid_payment等等。除非它們之間真的沒有任何區別(通常都有區別,但懶得一一想名字),就只是需要複數個同類型的物件

不要使用內建名稱

意即不要使用 Python 的保留字。理由可想而知,因為它會覆蓋原來的內建物件,造成程式的錯誤與混亂。

那真的需要怎麼辦?一個常見的做法是加上_作為後綴。比如type_

史上最糟的變數名稱——data

不瞞您說,萬事萬物都是資料。

在實務中,遇到把變數命名為 data 的情況還真不算罕見,往往讓人看得一頭霧水。

書中的這段話說得非常好(我稍重組了一下原句):

將變數命名為 data,就像把你的狗命名為 Dog。

簡言之,執意將變數命名為 data,需要非常堅實的理由——通常不存在這樣的理由。


第 5 章:找出程式碼的異味

程式碼異味(code smell)指的就是程式中看起來「不對勁」,且有可能在後續引發問題的地方。

如果可以,我們當然要盡可能避免異味,至少要給予一定關注才行。

實務上,我會在覺得有明顯異味的地方留下XXX註解,闡述異味的理由,並留意後續發展。

1
2
3
# XXX PATCH 做的事情太多了,很容易發生狀態殘留問題
if request.method == 'PATCH':
...

畢竟異味不一定都會造成問題,也不是所有的異味都要被修正(因為太多了),甚至隨著後續開發,原本看起來的異味可能隨之消散或減緩——但這屬於少見的情況。大部分都是愈來愈糟,不然怎麼會有「技術債」這個詞呢?

書中所舉的異味種類很多,也都很經典,值一一細讀。不過限於篇幅,只舉其中幾個我個人認為比較有代表性的異味。

一、重複的程式碼

重複程式碼肯定不是一個錯誤(error),卻很可能造成後續維護上的困擾。典型的困境就是改一個地方結果多處也要跟著一起改時,我稱之為程式碼的「同步」問題。

需要手動同步,就是壞味道。

通常我們會將這部分抽取出來,讓程式只需要改動一處,就能在所有引用處生效。

不過,書中也說了,重複一、兩次通常沒有關係;而重複三、四次時,就要好好考慮了是否重構或刪除。

而且有些時候,重複反而是比較簡潔的做法——無腦但直觀,可讀性最高。

確實,單憑形式上的「是否重複」來判斷要不要修改程式碼,往往是不足夠的。還要看使用的場景。比如前面說的那種會發生同步問題的重複,哪怕只有一次,我也會特地抽出來。

顯然,這需要一些經驗,與一次又一次的思考。

二、魔術數字

有些數字我們很容易知曉其含意,比如 3.14,而有些則否。

1
expiration = time.time() + 604800

這 604800 究竟代表什麼?我們肯定需要停下來思考,可能需要花一些時間才能想出來。這樣的寫法顯然太不直觀。

重點是,你縱使猜得到,也無法 100% 確定

一種改進方式是加上註解:

1
expiration = time.time() + 604800  # 一星期後失效

原來 604800 代表的是一週的秒數!

這樣寫好很多,但書中建議我們採取更經典的做法——使用常數

1
2
3
4
5
6
SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE
SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY

expiration = time.time() + SECONDS_PER_WEEK # 一星期後失效

常數是在程式中被使用的固定值,通常是不能被改變的。在使用常數時,我們應該使用有描述性的命名,這樣有助於提高程式的可讀性和可維護性。

常數 + 有描述性的命名,就是應對魔術數字的絕佳手段。

比如 Django REST Framework(DRF)中的status

1
return JsonResponse(context, status=status.HTTP_200_OK)

除了 200,常見的還有:

  • status.HTTP_400_BAD_REQUEST
  • status.HTTP_404_NOT_FOUND

我們當然可以直接寫status=200400404,看起來也很直觀,但對於那些我們沒那麼熟悉的 HTTP 狀態碼,比如 509,則又需要陷入停頓與思考。

而使用常數的話,則所有成員都會更加一視同仁

1
status.HTTP_509_BANDWIDTH_LIMIT_EXCEEDED

三、雙層生成式

書中的例子是,你想把一個二維 list「攤平」成一維,於是寫了雙層串列生成式(List Comprehension)進行處理,像這樣:

1
2
3
4
nested_list = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
flat_list = [num for sub_list in nested_list for num in sub_list]
>>> flat_list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

這樣的寫法雖然簡短,但可讀性極差。哪怕是 Python 老手,也很難一眼就看出來這個生成式到底在做什麼。

作者建議用雙層的 for 迴圈改寫,以增加可讀性:

1
2
3
4
5
6
7
8
nested_list = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]]
flat_list = []
for sub_list in nested_list:
for num in sub_list:
flat_list.append(num)

>>> flat_list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

確實,但說真的,可以的話,也盡量不要使用雙層迴圈。

其中的原因,容我直接引用臉書舊貼文

程式碼裡有:一層迴圈是壞味道,兩層迴圈是罪惡,三層迴圈是罪大惡極
 
因為這些都是程式中的「手工藝」,乏缺架構思維,除了當下用起來很「方便」,日後更可能是難以維護,畢竟正常人無法一眼就看出第二、三層迴圈到底在做什麼(仔細看也一樣啦!)
 
此乃典型的技術債,而且是很廉價的那種

雙重迴圈有時是不得已——總會有這樣的時候,但我們仍必須視之為罪惡

程式碼異味的迷思

這是第 5 章的最後一節,但我覺得特別精彩,值得獨立拿出來講。

書中所謂「程式碼異味的迷思」,指的是作者不認為那是異味,但常常會被視為異味的情況。

書中列舉了 5 個迷思(作者不認同的命題),分別如下:

  1. 迷思:函式末尾應該只有一個 return
  2. 迷思:函式中最多應該只有一個 try 陳述句
  3. 迷思:旗標(flag)引數是不好的
  4. 迷思:全域變數是不好的
  5. 迷思:注釋是不需要的

有些你可能也不覺得是迷思,沒關係,這裡我只打算討論第 5 個。

註解是不需要的?

我們可能聽過類似「好的程式碼應該透過優秀的命名與明確的意圖來自我表達、好的程式不需要註解」等等論述。

這堪稱是一種理想,乍聽也有幾分道理,但事實又如何呢?

在我看來,不寫任何註解或 docstring,卻期待程式非常好讀,幾乎是不可能的!

註解或 docstring 使用自然語言,表達能力與彈性遠遠超過受限於「用詞精簡且著重於功能面」的程式碼。

我在「Python docstring 之我見」也表述過類似的看法:

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

不知道你怎麼看?我相信,寫好 docstring,是簡潔程式碼不可或缺的一環,更是優秀軟體工程師的必備條件——我對此深信不疑。

註解與 docstring 的價值

有些事很重要,開發者在維護與修改程式時必須知道,這些事往往難以直接藉由程式碼透露出來,比如函式的「使用方式」與「注意事項」,這時註解就派上用場了。

退萬步言,複雜的程式光要看懂就很不容易。而良好的註解與 docstring 可以大幅縮短閱讀理解的時間,你的隊友一定會感謝你。

尤其在團隊開發時,註解與 docstring 是溝通的重要媒介,有時甚至是唯一的媒介。

當然我們也不得不承認,很多註解都寫得很爛!但這不是註解的錯,而是寫的人還不夠熟練或深思。而且寫好註解也真的很難,需要大量的練習。

顯然作者也是這麼認為的:

要寫出簡潔有效的注釋並不容易,注釋就像程式碼一樣,需要重寫和多次修調才會更好。

寫好註解是同理心的表現

就像我在〈18,論軟體工程師常見的「路徑依賴」問題(上)〉中所闡述的:

寫程式若只顧自己,終究難登大雅之堂。這些程式碼看起來就像是一個人的自言自語(只要自己有完成任務就好)——它並沒有在溝通

程式碼是溝通,而註解更是。它是同理心中「換位思考」的具體表現。

而且,把註解寫好,自己也會是受益者。因為我們不可能總是記得程式的所有細節,哪怕是自己寫的程式。

優秀的註解和 docstring,讓人讀程式時心曠神怡,維護的壓力也小得多。我相信任何開發者都希望自己接手的是這樣的程式碼。

在團隊協作中,這是一種任何人都會喜愛的溫柔。