Python 編碼風格指南

icebear發表於2016-03-30

本文超出 PEP8 的範疇以涵蓋我認為優秀的 Python 風格。本文雖然堅持己見,卻不偏執。不僅僅涉及語法、模組佈局等問題,同時深入正規化、組織及架構的領域。希望本文能成為精簡版 Python 程式碼《風格的要素》

目次

基本遵從 PEP 準則

…… 但是,命名和單行長度更靈活。

PEP8 涵蓋了諸如空格、函式/類/方法之間的換行、import、對已棄用功能的警告之類的尋常東西,大都不錯。

應用這些準則的最佳工具是 flake8,還可以用來發現一些愚蠢的語法錯誤。

PEP8 原本只是一組指導原則,不必嚴格甚至虔誠地信奉。一定記得閱讀 PEP8 「愚蠢的一致性就是小人物的小妖精」一節。若要進一步瞭解,可以聽一下 Raymond Hettinger 的精彩演講,「超越 PEP8」

唯一引起過多爭議的準則事關單行長度和命名。要調整起來也不難。

靈活的單行長度

若是厭煩 flake8 死板的單行長度不得超過 79 個字元的限制,完全可以忽略或修改這一準則。這仍然不失為一條不錯的經驗法則,就像英語中句子不能超過 50 個單詞,段落不能超過 10 個句子之類的規則一樣。這是 flake8 配置檔案 的連結,可以看到 max-line-length 配置選項。值得注意的是,可以給要忽略 flake8 檢查的那一行加上 # noqa 註釋,但是請勿濫用。

儘管如此,超過九成的程式碼行都不應該超過 79 個字元,原因很簡單,「扁平勝於巢狀」。如果函式每一行都超出了 79 個字元,肯定有別的東西出錯了,這時要看看程式碼而不是 flake8 配置。

一致的命名

關於命名,遵循幾條簡單的準則就可以避免眾多足以影響整個小組的麻煩。

推薦的命名規則

下面這些準則大多改編自 Pacoo 小組

  • 類名:駝峰式 和首字母縮略詞:HTTPWriter 優於 HttpWriter
  • 變數名:lower_with_underscores
  • 方法名和函式名:lower_with_underscores
  • 模組名:lower_with_underscores.py。(但是不帶下劃線的名字更好!)
  • 常量名:UPPER_WITH_UNDERSCORES
  • 預編譯的正規表示式:name_re

通常都應該遵循這些準則,除非要參照其他工具的命名規範,比如資料庫 schema 或者訊息格式。

還可以用 駝峰式 給類似類卻不是類的東西命名。使用 駝峰式 的主要好處在於讓人們以「全域性名詞」來關注某個東西,而不是看作區域性標記或動詞。值得注意的是,Python 給 TrueFalseNone 這些根本不是類的東西命名也是用 駝峰式

不要用字首字尾

…… 比如 _prefixsuffix_ 。函式和方法名可以用 _prefix 標記來暗示其是「私有的」,但是最好只在編寫預期會廣泛使用的 API 以及用 _prefix 標記來隱藏資訊的時候謹慎使用。

PEP8 建議使用結尾的下劃線來避免與內建關鍵字重名,比如:

臨時這樣用也可以,不過最好還是選一個別的名字。

__mangled 這種雙下劃線字首給類/例項/方法命名的情況非常少,這實際上涉及特殊的名字修飾,非常罕見。不要起 __dunder__ 這種格式的名字,除非要實現 Python 標準協議,比如 __len__;這是為 Python 內部協議保留的名稱空間,不應該在其中增加自定義的東西。

不要用單字元名字

(不過)一些常見的單字元名字可以接受。

lambda 表示式中,單引數函式可以命名為 x 。比如:

解包元組時可以用 _ 丟棄不需要的標記。比如:

意思就是說「忽略第一個元素」。

lambda 類似,在解析列表/字典/集合的時候,以及在生成器表示式或者一到兩行的 for 迴圈中,可以使用單字元迭代標記。通常選擇 x,比如:

可以求 items 序列中所有正整數之和。

此外比較常見的是 i,代表 index,通常和內建的 列舉 一起使用。比如:

除卻上述情形,要極少甚至避免使用單字元用作標記/引數/方法的名字。因為這樣就無法用 grep 進行檢索了。

使用 self 及類似的慣例

應該:

  • 永遠將方法的第一個變數命名為 self
  • 永遠將 @classmethod 的第一個引數命名為 cls
  • 永遠在變數引數列表中使用 *args**kwargs

不要在這些地方吹毛求疵

不遵循如下準則沒有什麼好處,乾脆照它說的做。

永遠繼承自 object 並使用新式類

對於 Python 2 來說遵循這條準則很重要。不過由於 Python 3 所有的類都隱式繼承自 object,這條準則就沒有必要了。

不要在類中重複使用例項標記

列表/字典/集合推導式優於 map/filter

儘管在大多數簡單情況下最好使用解析表示式,不過有時候 map() 或者 filter() 可讀性更佳,需要自己判斷。

用圓括號 (...) 折行

用圓括號 (...) 寫 API 更利落

函式呼叫中使用隱式行延續

isinstance(obj, cls), 不要用 type(obj) == cls

因為 isinstance 涵蓋更多情形,包括子類和抽象基類。同時,不要過多使用 isinstance ,因為通常應該使用鴨子型別!

with 處理檔案和鎖

with 語句能夠巧妙地關閉檔案並釋放鎖,哪怕是在觸發異常的情況下。所以:

None 相比較要用 is

None 值是一個單例,但是檢查 None 的時候,實際上很少真的要在 左值上呼叫 __eq__。所以:

好的寫法不僅執行更快,而且更準確。使用 == 並不會更簡潔,所以請記住本規則!

不要修改 sys.path

通過 sys.path.insert(0, "../") 等操作來控制 Python 的匯入方法或許讓人心動,但是要堅決避免這樣做。

Python 有一套有幾分複雜,卻易於理解的模組路徑解決方法。可以通過 PYTHONPATH 或諸如 setup.py develop 的技巧來調整 Python 匯入模組的方法。還可以用 -m 執行 Python 得到需要的效果,比如使用 python -m mypkg.mymodule 而不是 python mypkg/mymodule.py。程式碼能否正常執行不應依賴於當前執行 Python 的工作路徑。David Beazley 用 PDF 幻燈片再一次扭轉了大家的看法,值得一讀,“Modules and Packages: Live and Let Die!”

儘量不要自定義異常型別

…… 如果一定要,也不要建立太多。

要知道 Python 引入了 豐富的內建異常類。值得充分利用。而且通過描述那些觸發特定錯誤條件的字串訊息,來例項化這些異常類,就能達到「定製」的目的。在使用者程式碼中丟擲 ValueError(引數錯誤),LookupError(鍵錯誤)以及 AssertionError(用 assert 語句)最為常見。

至於是否應該自己建立異常類有一個不錯的經驗法則,也就是搞清楚函式呼叫方每次呼叫函式之時是否都應該捕獲該異常。如果是,那麼的確應該自己建立異常類。不過這相當少見。關於這類明顯不得不使用的自定義異常類有一個不錯的例子,tornado.web.HTTPError。但是要留心 Tornado 是如何避免走極端的:框架或使用者程式碼丟擲的所有 HTTP 錯誤同屬一個異常類。

短文件字串應是名副其實的單行句子

把三引號 """ 放在同一行,首字母大寫,以句號結尾。四行精簡到兩行,__doc__ 屬性沒有糟糕的換行,最吹毛求疵的人也會滿意的!

文件字串使用 reST

標準庫和大多數開源專案皆是如此。Sphinx 提供支援,開箱即用。趕緊試試吧!Python requests 模組由此取得了極佳的效果。看看requests.api 模組的例子。

刪除結尾空格

最挑剔也不過如此了吧,可是若做不到這一點,有些人可能會被逼瘋。不乏能自動搞定這一切的編輯器;這是我用 vim 的實現

文件字串要寫好

下面是在函式文件字串中使用 Sphinx 風格的 reST 的快速參考:

不要為文件而寫文件。寫文件字串要這樣思考:

也就是說,上例中沒有必要說 timeoutfloat,預設值 5.0,顯然是 float。在文件中指出其語義是「秒」更有用,就是說 5.0 意思是 5 秒鐘。同時呼叫方不知道 qsargs 應該是什麼,所以用 type 註釋給出提示,呼叫方也無從知道函式的預期返回值是什麼,所以 rtype 註釋是合適的。

最後一點。吉多·範羅蘇姆曾說過,他對 Python 的主要領悟是「讀程式碼比寫程式碼頻率更高」。直接結論就是有些文件有用,更多的文件有害

基本上只需要給預計會頻繁重用的函式寫文件。如果給內部模組的每一個函式都寫上文件,最後只能得到更加難以維護的模組,因為重構程式碼之時文件也要重構。不要「船貨崇拜」文件字串,更不要用工具自動生成文件。

正規化和模式

是函式還是類

通常應該用函式而不是類。函式和模組是 Python 程式碼重用的基本單元,還是最靈活的形式。類是一些 Python 功能的「升級路徑」,比如實現容器,代理,描述符,型別系統等等。但是通常函式都是更好的選擇。

或許有人喜歡為了更好地組織程式碼而將關聯的函式歸在類中。但這是錯的。關聯的函式應該歸在模組中。

儘管有時可以把類當作「小型名稱空間」(比如用 @staticmethod)比較有用,一組方法更應該對同一個物件的內部操作有所貢獻,而不僅僅作為行為分組。

與其建立 TimeHelper 類,帶有一堆不得不引入子類才能使用的方法,永遠不如直接為時間相關的函式建立 lib.time 模組。類會增殖出更多的類,會增加複雜性,降低可讀性。

生成器和迭代器

生成器和迭代器是 Python 中最強大的特性 —— 應該掌握迭代器協議,yield 關鍵字和生成器表示式。

生成器不僅僅對要在大型資料流上反覆呼叫的函式十分重要,而且可以讓自定義迭代器更加簡單,從而簡化了程式碼。將程式碼重構為生成器通常可以在使得程式碼在更多場景下複用,從而簡化程式碼。

Fluent Python 的作者 Lucinao Ramalho 通過 30 分鐘的演講,「迭代器和生成器: Python 之道」,給出了一個出色的,快節奏的概述。Python Essential Reference 和 Python Cookbook 的作者 David Beazley 有個深奧的三小時視訊教程,「生成器:最後的前沿」,給出了令人滿足的生成器用例的詳細闡述。因為應用廣泛,掌握這一主題非常有必要。

宣告式還是命令式

宣告式程式設計優於指令式程式設計。程式碼應該表明你想要做什麼,而不是描述如何去做。Python 的函數語言程式設計概覽介紹了一些不錯的細節並給出了高效使用該風格的例子。

使用輕量級的資料結構更好,比如 列表字典元組集合。將資料展開,編寫程式碼對其進行轉換,永遠要優於重複呼叫轉換函式/方法來構建資料。

一個例子是重構常見的列表解析:

應該改成 :

但是還有一個不錯的例子是將 if/elif/else 鏈改成 字典 查詢。

「純」函式和迭代器更好

這是個從函數語言程式設計社群借來的概念。這種函式和迭代器亦被描述為「無副作用」,「引用透明」或者有「不可變輸入/輸出」。

一個簡單的例子,要避免這種程式碼:

這個函式應該重新寫成:

這是個驚人的例子。函式不僅更加純粹,而且更加精簡了。不僅更加精簡,而且更好。這裡的純粹是說 assert dedupe(items) == dedupe(items) 在「好」版本中恆為真。在「壞」版本中, num_dupes 在第二次呼叫時0,這會在使用時導致難以理解的錯誤。

這個例子也闡明瞭命令式風格和宣告式風格的區別:改寫後的函式讀起來更像是對需要的東西的描述,而不是構建需要的東西的一系列操作。

簡單的引數和返回值型別更好

函式應該儘可能處理資料,而不是自定義的物件。簡單的引數型別更好,比如字典集合元組列表intfloatbool。從這些擴充套件到標準庫型別,比如 datetime, timedelta, array, Decimal 以及 Future。只有在真的必要時才使用自定義型別。

判斷函式是否足夠精簡有個不錯的經驗法則,問自己引數和返回值是否總是可以 JSON 序列化。結果證明這個經驗法則相當有用:可以 JSON 序列化通常是函式在平行計算時可用的先決條件。但是,就本文件而言,主要的好處在於:可讀性,可測試性以及總體的函式簡單性。

避免「傳統的」物件導向程式設計

在「傳統的物件導向程式語言」中,比如 Java 和 C++ ,程式碼重用是通過類的繼承和多型或者語言聲稱的類似機制實現的。對 Python 而言,儘管可以使用子類和基於類的多型,事實上在地道的 Python 程式中這些功能極少使用。

通過模組和函式實現程式碼重用更為普遍,通過鴨子型別實現動態排程更為常見。如果發現自己通過超類實現程式碼重用,停下來,重新思考。如果發現自己大量使用多型,考慮一下是否用 Python 的 dunder 協議或者鴨子型別策略會更好。

看一下另一個不錯的 Python 演講,一位 Python 核心貢獻者的 「不要再寫類了」。演講者建議,如果構建的類只有一個命名像一個類的方法(比如 Runnable.run()),那麼實際上只是用函式模擬了一個類,這時候應該停下來。因為在 Python 中,函式是「最高階的」型別,沒有理由這樣做。

Mixin 有時也沒問題

可以使用 Mixin 實現基於類的程式碼重用,同時不需要走極端使用型別層次。但是不要濫用。「扁平勝於巢狀」也適用於型別層次,所以應該避免僅僅為了分解行為而引入不必要的必須層次的一層。

Mixin 實際上不是 Python 的特性,多虧了 Python 支援多重繼承。可以建立基類將功能「注入」到子類中,而不必構成型別層次的「重要」組成部分,只需要將基類列入 bases 列表中的第一個元素。

要考慮順序,同時不妨記住:bases 自底向上構成層次結構。這裡可讀性的好處在於關於這個類所需要知道的一切都包含在定義本身:「它混入了許可權行為,是專門定製的 Tornado RequestHandler。」

小心框架

Python 有大量的 web,資料庫等框架。Python 語言的一大樂趣在於建立自定義框架相當簡單。使用開源框架時,應該注意不要將自己的「核心程式碼」和框架本身結合得過於緊密。

考慮為自己的程式碼建立框架的時候應當慎之又慎。標準庫有很多內建的東西,PyPI 有的就更多了,而且通常你不會需要它

尊重超程式設計

Python 通過一些特性來支援 「超程式設計」,包括修飾器,上下文管理器,描述符,import 鉤子,元類和抽象語法樹(AST)轉換。

應該能夠自如地使用並理解這些特性,作為 Python 的核心組成部分這些特性有著充分地支援。但是應當意識到使用這些特性之時,也引入了複雜的失敗場景。所以,要把為自己的程式碼建立超程式設計工具與決定「建立自定義框架」同等對待。它們意味著同一件事情。真要這麼做的時候,把超程式設計工具寫成獨立的模組,寫好文件!

不要害怕 「雙下劃線」方法

許多人將 Python 的超程式設計工具和其對 「雙下劃線」或「dunder」方法(比如 __getattr__)的支援混為一談。

正如博文 ——「Python 雙下劃線,雙倍驚喜」—— 所言,雙下劃線沒有什麼「特殊的」。它們只不過是 Python 核心開發人員為所有的 Python 內部協議所起的輕量級名稱空間。畢竟,__init__ 也是雙下劃線,沒有什麼神奇的。

的確,有些雙下劃線比其他的會導致更多令人困惑的結果,比如,沒有很好的理由就過載操作符通常不是什麼好主意。但是它們中也有許多,比如 __repr____str____len__ 以及 __call__ 是 Python 語言的完整組成部分,應該在地道的 Python 程式碼中充分利用。不要回避!

程式碼風格小禪理

作為一位核心 Python 開發者,Barry Warsaw 曾經說過「Python 之禪」(PEP 20)被用作 Python 程式碼風格指南使他沮喪,因為這本是為 Python 的內部設計所作的一首小詩。也就是語言的設計以及語言的實現本身。不過必須承認,PEP 20 中有些行可以當作相當不錯的地道 Python 程式碼指南,所以我們就把它加上了。

美勝於醜

這一條有些主觀,實際上等同於問:接手程式碼的人會被折服還是感到失望?如果接手的人就是三年後的你呢?

顯勝於隱

有時為了重構以去除重複的程式碼,會有一點抽象。應該能夠將程式碼翻譯成顯現的英語並且大致瞭解它是幹什麼的。不應該有太多的「神奇之處」。

扁平勝於巢狀

這一條很好理解。最好的函式沒有巢狀,既不用迴圈也不用 if 語句。第二好的函式只有一層巢狀。如果有兩層及以上的巢狀,最好重構成更小的函式。

同樣,不要害怕將巢狀的 if 語句重構為多部分佈爾條件。比如:

最好寫成:

可讀性確實重要

不要害怕用 # 新增行註釋。也不要濫用或者寫過多文件。一點點逐行解釋,通常很有幫助。不要害怕使用稍微長一些的名字,因為描述性更好。將 「response」寫成「rsp」沒有任何好處。使用 doctest 風格的例子在文件字串中詳細說明邊界情況。簡潔至上!

錯誤不應被放過

單獨的except: pass 子句危害最大。永遠不要使用。制止所有的異常實在危險。將異常處理限制在一行程式碼,並且總是將 except 處理器限制在特定的型別下。除此之外,可以自如地使用 logging 模組和 log.exception(…)

如果實現難以解釋,那就是個壞主意

這雖是通用軟體工程原則,但是特別適用於 Python 程式碼。大多數 Python 函式和物件都可以有易於解釋的實現。如果難以解釋,很可能是一個壞主意。通常可以通過「分而治之」將一個難以解釋的函式重寫成易於解釋的函式,也就是分割成多個函式。

測試是個好主意

好吧,我們篡改了「Python 之禪」中的這一行,原文中「名稱空間」才是個絕妙的好主意。

不過說正經的,優雅卻沒有測試的程式碼簡直比哪怕是最醜陋卻測試過的程式碼還要差勁。至少醜陋的程式碼可以重構成優雅的,但是優雅的程式碼卻不能重構為可以證明是正確的程式碼,至少不寫測試是做不到的!所以,寫測試吧!拜託!

平分秋色

我們把寧願不去解決的爭論放在這個部分。不要因為這些重寫別人的程式碼。這裡的東西可以自由地交替使用。

str.format 還是過載格式化操作符 %

str.format 更健壯,然而 % 使用 "%s %s" printf 風格的字串更加簡潔。兩者會永遠共存。

如果需要儲存 unicode,記得在格式模板中使用 unicode 字串:

如果最後選擇 %,應該考慮 "%(name)s" 語法,從而可以使用字典而不是元組,比如:

此外,不要重新發明輪子。str.format 有一點毫無疑問比 % 更好,那就是支援各種格式化模式,比如人性化的數字和百分數。直接用。

但是選擇哪一個都沒有問題。我們沒有強制規定。

if item 還是 if item is not None

本條和之前的對於 None 是用 == 還是 is 沒有關係。這裡我們實際上利用了 Python 的 「真實性規則」來處理 if item,這實際上是「item 不是 None 或者空字串」的簡寫。

Python 中的真實性有些複雜。顯然第二種寫法對於某些錯誤而言更安全。但是第一種寫法在 Python 程式碼中非常常見,而且更短。對此我們並沒有強制規定。

隱式多行字串還是三引號 """

Python 編譯器在語法分析時,如果多個字串之間沒有東西,會自動將其拼接為一個字串。比如:

大致上等價於:

第一種寫法可以保持縮排整潔,但是需要醜陋的換行符,第二種寫法不需要換行符,但是打破了縮排。我們沒有強制規定哪種更好。

在類還是例項上使用 raise

事實上給 raise 語句傳入異常或者異常例項都可以。比如,下面兩行程式碼大致等價:

本質上,Python 會將第一種寫法自動轉換為第二種。或許第二個寫法更好,即使沒有其他原因,它也能實際上提供一個有用的理由,就像有條有用的訊息來解釋為什麼 ValueError 會發生一樣。但是這兩種寫法等價的,不應該為這一點就重寫程式碼。我們不做強制規定。

標準工具和專案結構

我們選擇了一些「最佳組合」工具,以及像樣的 Python 專案會用到的最小初始結構。

標準庫

  • import datetime as dt: 永遠像這樣匯入 datetime
  • dt.datetime.utcnow(): 優於 .now(), 後者使用的是當地時間
  • import json: 資料交換的標準
  • from collections import namedtuple: 用來做輕量級資料型別
  • from collections import defaultdict: 用來計數/分組
  • from collections import deque: 快速的雙向佇列
  • from itertools import groupby, chain: 為了宣告式風格
  • from functools import wraps: 用來編寫合乎規範的裝飾器
  • argparse: 為了構建「健壯的」命令列工具
  • fileinput: 用來快速構建易於和 UNIX 管道結合使用的工具
  • log = logging.getLogger(__name__): 足夠好用的日誌
  • from __future__ import absolute_import: 修復匯入別名

常見第三方庫

  • python-dateutil 用來解析時間和日曆
  • pytz 用來處理時區
  • tldextract 為了更好地處理 URL
  • msgpack-python 比 JSON 更加緊湊地編碼
  • futures 為了 Future/pool 併發原語
  • docopt 用來快速編寫一次性命令列工具
  • py.test 用來做單元測試,與 mockhypothesis 結合使用

本地開發專案框架

對所有的 Python 包和庫而言:

  • 根目錄下不要有 __init__.py:目錄名用作包名!
  • mypackage/__init__.py 優於 src/mypackage/__init__.py
  • mypackage/lib/__init__.py 優於 lib/__init__.py
  • mypackage/settings.py 優於 settings.py
  • README.rst 用來給新手描述本專案;使用 rst
  • setup.py 用來構建簡單工具,比如 setup.py develop
  • requirements.txt 是為 pip 準備的包依賴環境
  • dev-requirements.txt 是為 tests/local 準備的額外的依賴環境
  • Makefile 用來簡化 (!!!) build/lint/test/run 步驟

另外,永遠記得詳細說明包依賴環境

靈感來源

下面這些連結或許可以給你啟發,有助於書寫具有良好風格和品味的 Python 程式碼。

出發吧,寫更具 Python 風格的程式碼!

撰稿人

  • Andrew Montalenti (@amontalenti): 原作者
  • Vincent Driessen (@nvie): 編輯並提出意見

喜歡優雅的 Python 風格嗎?歡迎加入我們在 Parse.ly 公司的 Pythonistas 小組!

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

Python 編碼風格指南 Python 編碼風格指南

相關文章