讓你的 Python 程式碼優雅又地道

發表於2017-09-07

譯序

如果說優雅也有缺點的話,那就是你需要艱鉅的工作才能得到它,需要良好的教育才能欣賞它。

—— Edsger Wybe Dijkstra

在Python社群文化的澆灌下,演化出了一種獨特的程式碼風格,去指導如何正確地使用Python,這就是常說的pythonic。一般說地道(idiomatic)的python程式碼,就是指這份程式碼很pythonic。Python的語法和標準庫設計,處處契合著pythonic的思想。而且Python社群十分注重編碼風格一的一致性,他們極力推行和處處實踐著pythonic。所以經常能看到基於某份程式碼P vs NP (pythonic vs non-pythonic)的討論。pythonic的程式碼簡練,明確,優雅,絕大部分時候執行效率高。閱讀pythonic的程式碼能體會到“程式碼是寫給人看的,只是順便讓機器能執行”暢快。

然而什麼是pythonic,就像什麼是地道的漢語一樣,切實存在但標準模糊。import this可以看到Tim Peters提出的Python之禪,它提供了指導思想。許多初學者都看過它,深深贊同它的理念,但是實踐起來又無從下手。PEP 8給出的不過是編碼規範,對於實踐pythonic還遠遠不夠。如果你正被如何寫出pythonic的程式碼而困擾,或許這份筆記能給你幫助。

Raymond Hettinger是Python核心開發者,本文提到的許多特性都是他開發的。同時他也是Python社群熱忱的佈道師,不遺餘力地傳授pythonic之道。這篇文章是網友Jeff Paine整理的他在2013年美國的PyCon的演講的筆記。

術語澄清:本文所說的集合全都指collection,而不是set

以下是正文。


本文是Raymond Hettinger在2013年美國PyCon演講的筆記(視訊, 幻燈片)。

示例程式碼和引用的語錄都來自Raymond的演講。這是我按我的理解整理出來的,希望你們理解起來跟我一樣順暢!

遍歷一個範圍內的數字

更好的方法

xrange會返回一個迭代器,用來一次一個值地遍歷一個範圍。這種方式會比range更省記憶體。xrange在Python 3中已經改名為range

遍歷一個集合

更好的方法

反向遍歷

更好的方法

遍歷一個集合及其下標

更好的方法

這種寫法效率高,優雅,而且幫你省去親自建立和自增下標。

當你發現你在操作集合的下標時,你很有可能在做錯事。

遍歷兩個集合

更好的方法

zip在記憶體中生成一個新的列表,需要更多的記憶體。izipzip效率更高。

注意:在Python 3中,izip改名為zip,並替換了原來的zip成為內建函式。

有序地遍歷

自定義排序順序

更好的方法

第一種方法效率低而且寫起來很不爽。另外,Python 3已經不支援比較函式了。

呼叫一個函式直到遇到標記值

更好的方法

iter接受兩個引數。第一個是你反覆呼叫的函式,第二個是標記值。

譯註:這個例子裡不太能看出來方法二的優勢,甚至覺得partial讓程式碼可讀性更差了。方法二的優勢在於iter的返回值是個迭代器,迭代器能用在各種地方,setsortedminmaxheapqsum……

在迴圈內識別多個退出點

更好的方法

for執行完所有的迴圈後就會執行else

譯註:剛瞭解for-else語法時會困惑,什麼情況下會執行到else裡。有兩種方法去理解else。傳統的方法是把for看作if,當for後面的條件為False時執行else。其實條件為False時,就是for迴圈沒被break出去,把所有迴圈都跑完的時候。所以另一種方法就是把else記成nobreak,當for沒有被break,那麼迴圈結束時會進入到else

遍歷字典的key

什麼時候應該使用第二種而不是第一種方法?當你需要修改字典的時候。

如果你在迭代一個東西的時候修改它,那就是在冒天下之大不韙,接下來發生什麼都活該。

d.keys()把字典裡所有的key都複製到一個列表裡。然後你就可以修改字典了。

注意:如果在Python 3裡迭代一個字典你得顯示地寫:list(d.keys()),因為d.keys()返回的是一個“字典檢視”(一個提供字典key的動態檢視的迭代器)。詳情請看文件

遍歷一個字典的key和value

更好的方法

iteritems()更好是因為它返回了一個迭代器。

注意:Python 3已經沒有iteritems()了,items()的行為和iteritems()很接近。詳情請看文件

用key-value對構建字典

Python 3: d = dict(zip(names, colors))

用字典計數

更好的方法

用字典分組 — 第I部分和第II部分

更好的方法

字典的popitem()是原子的嗎?

popitem是原子的,所以多執行緒的時候沒必要用鎖包著它。

連線字典

更好的方法

ChainMap在Python 3中加入。高效而優雅。

提高可讀性

  • 位置引數和下標很漂亮
  • 但關鍵字和名稱更好
  • 第一種方法對計算機來說很便利
  • 第二種方法和人類思考方式一致

用關鍵字引數提高函式呼叫的可讀性

更好的方法

第二種方法稍微(微秒級)慢一點,但為了程式碼的可讀性和開發時間,值得。

namedtuple提高多個返回值的可讀性

更好的方法

namedtupletuple的子類,所以仍適用正常的元組操作,但它更友好。

建立一個nametuple

unpack序列

更好的方法

第二種方法用了unpack元組,更快,可讀性更好。

更新多個變數的狀態

更好的方法

第一種方法的問題

  • x和y是狀態,狀態應該在一次操作中更新,分幾行的話狀態會互相對不上,這經常是bug的源頭。
  • 操作有順序要求
  • 太底層太細節

第二種方法抽象層級更高,沒有操作順序出錯的風險而且更效率更高。

同時狀態更新

更好的方法

效率

  • 優化的基本原則
  • 除非必要,別無故移動資料
  • 稍微注意一下用線性的操作取代O(n**2)的操作

總的來說,不要無故移動資料

連線字串

更好的方法

更新序列

更好的方法

裝飾器和上下文管理

  • 用於把業務和管理的邏輯分開
  • 分解程式碼和提高程式碼重用性的乾淨優雅的好工具
  • 起個好名字很關鍵
  • 記住蜘蛛俠的格言:能力越大,責任越大

使用裝飾器分離出管理邏輯

更好的方法

注意:Python 3.2開始加入了functools.lru_cache解決這個問題。

分離臨時上下文

更好的方法

譯註:示例程式碼在使用標準庫decimal,這個庫已經實現好了localcontext

如何開啟關閉檔案

更好的方法

如何使用鎖

更好的方法

分離出臨時的上下文

更好的方法

ignored是Python 3.4加入的, 文件

注意:ignored 實際上在標準庫叫suppress(譯註:contextlib.supress).

試試建立你自己的ignored上下文管理器。

把它放在你的工具目錄,你也可以忽略異常

譯註:contextmanager在標準庫contextlib中,通過裝飾生成器函式,省去用__enter____exit__寫上下文管理器。詳情請看文件

分離臨時上下文

更好的寫法

redirect_stdout在Python 3.4加入(譯註:contextlib.redirect_stdout), bug反饋

實現你自己的redirect_stdout上下文管理器。

簡潔的單句表達

兩個衝突的原則:

  • 一行不要有太多邏輯
  • 不要把單一的想法拆分成多個部分

Raymond的原則:

  • 一行程式碼的邏輯等價於一句自然語言

列表解析和生成器

更好的方法

第一種方法說的是你在做什麼,第二種方法說的是你想要什麼。

相關文章