譯序
如果說優雅也有缺點的話,那就是你需要艱鉅的工作才能得到它,需要良好的教育才能欣賞它。
—— 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的演講。這是我按我的理解整理出來的,希望你們理解起來跟我一樣順暢!
遍歷一個範圍內的數字
1 2 3 4 5 |
for i in [0, 1, 2, 3, 4, 5]: print i ** 2 for i in range(6): print i ** 2 |
更好的方法
1 2 |
for i in xrange(6): print i ** 2 |
xrange會返回一個迭代器,用來一次一個值地遍歷一個範圍。這種方式會比range更省記憶體。xrange在Python 3中已經改名為range。
遍歷一個集合
1 2 3 4 |
colors = ['red', 'green', 'blue', 'yellow'] for i in range(len(colors)): print colors[i] |
更好的方法
1 2 |
for color in colors: print color |
反向遍歷
1 2 3 4 |
colors = ['red', 'green', 'blue', 'yellow'] for i in range(len(colors)-1, -1, -1): print colors[i] |
更好的方法
1 2 |
for color in reversed(colors): print color |
遍歷一個集合及其下標
1 2 3 4 |
colors = ['red', 'green', 'blue', 'yellow'] for i in range(len(colors)): print i, '--->', colors[i] |
更好的方法
1 2 |
for i, color in enumerate(colors): print i, '--->', color |
這種寫法效率高,優雅,而且幫你省去親自建立和自增下標。
當你發現你在操作集合的下標時,你很有可能在做錯事。
遍歷兩個集合
1 2 3 4 5 6 7 8 9 |
names = ['raymond', 'rachel', 'matthew'] colors = ['red', 'green', 'blue', 'yellow'] n = min(len(names), len(colors)) for i in range(n): print names[i], '--->', colors[i] for name, color in zip(names, colors): print name, '--->', color |
更好的方法
1 2 |
for name, color in izip(names, colors): print name, '--->', color |
zip在記憶體中生成一個新的列表,需要更多的記憶體。izip比zip效率更高。
注意:在Python 3中,izip改名為zip,並替換了原來的zip成為內建函式。
有序地遍歷
1 2 3 4 5 6 7 8 9 |
colors = ['red', 'green', 'blue', 'yellow'] # 正序 for color in sorted(colors): print colors # 倒序 for color in sorted(colors, reverse=True): print colors |
自定義排序順序
1 2 3 4 5 6 7 8 |
colors = ['red', 'green', 'blue', 'yellow'] def compare_length(c1, c2): if len(c1) < len(c2): return -1 if len(c1) > len(c2): return 1 return 0 print sorted(colors, cmp=compare_length) |
更好的方法
1 |
print sorted(colors, key=len) |
第一種方法效率低而且寫起來很不爽。另外,Python 3已經不支援比較函式了。
呼叫一個函式直到遇到標記值
1 2 3 4 5 6 |
blocks = [] while True: block = f.read(32) if block == '': break blocks.append(block) |
更好的方法
1 2 3 |
blocks = [] for block in iter(partial(f.read, 32), ''): blocks.append(block) |
iter接受兩個引數。第一個是你反覆呼叫的函式,第二個是標記值。
譯註:這個例子裡不太能看出來方法二的優勢,甚至覺得partial讓程式碼可讀性更差了。方法二的優勢在於iter的返回值是個迭代器,迭代器能用在各種地方,set,sorted,min,max,heapq,sum……
在迴圈內識別多個退出點
1 2 3 4 5 6 7 8 9 |
def find(seq, target): found = False for i, value in enumerate(seq): if value == target: found = True break if not found: return -1 return i |
更好的方法
1 2 3 4 5 6 7 |
def find(seq, target): for i, value in enumerate(seq): if value == target: break else: return -1 return i |
for執行完所有的迴圈後就會執行else。
譯註:剛瞭解for-else語法時會困惑,什麼情況下會執行到else裡。有兩種方法去理解else。傳統的方法是把for看作if,當for後面的條件為False時執行else。其實條件為False時,就是for迴圈沒被break出去,把所有迴圈都跑完的時候。所以另一種方法就是把else記成nobreak,當for沒有被break,那麼迴圈結束時會進入到else。
遍歷字典的key
1 2 3 4 5 6 7 8 |
d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'} for k in d: print k for k in d.keys(): if k.startswith('r'): del d[k] |
什麼時候應該使用第二種而不是第一種方法?當你需要修改字典的時候。
如果你在迭代一個東西的時候修改它,那就是在冒天下之大不韙,接下來發生什麼都活該。
d.keys()把字典裡所有的key都複製到一個列表裡。然後你就可以修改字典了。
注意:如果在Python 3裡迭代一個字典你得顯示地寫:list(d.keys()),因為d.keys()返回的是一個“字典檢視”(一個提供字典key的動態檢視的迭代器)。詳情請看文件。
遍歷一個字典的key和value
1 2 3 4 5 6 7 |
# 並不快,每次必須要重新雜湊並做一次查詢 for k in d: print k, '--->', d[k] # 產生一個很大的列表 for k, v in d.items(): print k, '--->', v |
更好的方法
1 2 |
for k, v in d.iteritems(): print k, '--->', v |
iteritems()更好是因為它返回了一個迭代器。
注意:Python 3已經沒有iteritems()了,items()的行為和iteritems()很接近。詳情請看文件。
用key-value對構建字典
1 2 3 4 5 |
names = ['raymond', 'rachel', 'matthew'] colors = ['red', 'green', 'blue'] d = dict(izip(names, colors)) # {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'} |
Python 3: d = dict(zip(names, colors))
用字典計數
1 2 3 4 5 6 7 8 9 10 |
colors = ['red', 'green', 'red', 'blue', 'green', 'red'] # 簡單,基本的計數方法。適合初學者起步時學習。 d = {} for color in colors: if color not in d: d[color] = 0 d[color] += 1 # {'blue': 1, 'green': 2, 'red': 3} |
更好的方法
1 2 3 4 5 6 7 8 |
d = {} for color in colors: d[color] = d.get(color, 0) + 1 # 稍微潮點的方法,但有些坑需要注意,適合熟練的老手。 d = defaultdict(int) for color in colors: d[color] += 1 |
用字典分組 — 第I部分和第II部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
names = ['raymond', 'rachel', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie'] # 在這個例子,我們按name的長度分組 d = {} for name in names: key = len(name) if key not in d: d[key] = [] d[key].append(name) # {5: ['roger', 'betty'], 6: ['rachel', 'judith'], 7: ['raymond', 'matthew', 'melissa', 'charlie']} d = {} for name in names: key = len(name) d.setdefault(key, []).append(name) |
更好的方法
1 2 3 4 |
d = defaultdict(list) for name in names: key = len(name) d[key].append(name) |
字典的popitem()是原子的嗎?
1 2 3 4 5 |
d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'} while d: key, value = d.popitem() print key, '-->', value |
popitem是原子的,所以多執行緒的時候沒必要用鎖包著它。
連線字典
1 2 3 4 5 6 7 8 9 10 11 12 |
defaults = {'color': 'red', 'user': 'guest'} parser = argparse.ArgumentParser() parser.add_argument('-u', '--user') parser.add_argument('-c', '--color') namespace = parser.parse_args([]) command_line_args = {k: v for k, v in vars(namespace).items() if v} # 下面是通常的作法,預設使用第一個字典,接著用環境變數覆蓋它,最後用命令列引數覆蓋它。 # 然而不幸的是,這種方法拷貝資料太瘋狂。 d = defaults.copy() d.update(os.environ) d.update(command_line_args) |
更好的方法
1 |
d = ChainMap(command_line_args, os.environ, defaults) |
ChainMap在Python 3中加入。高效而優雅。
提高可讀性
- 位置引數和下標很漂亮
- 但關鍵字和名稱更好
- 第一種方法對計算機來說很便利
- 第二種方法和人類思考方式一致
用關鍵字引數提高函式呼叫的可讀性
1 |
twitter_search('@obama', False, 20, True) |
更好的方法
1 |
twitter_search('@obama', retweets=False, numtweets=20, popular=True) |
第二種方法稍微(微秒級)慢一點,但為了程式碼的可讀性和開發時間,值得。
用namedtuple提高多個返回值的可讀性
1 2 3 4 |
# 老的testmod返回值 doctest.testmod() # (0, 4) # 測試結果是好是壞?你看不出來,因為返回值不清晰。 |
更好的方法
1 2 3 |
# 新的testmod返回值, 一個namedtuple doctest.testmod() # TestResults(failed=0, attempted=4) |
namedtuple是tuple的子類,所以仍適用正常的元組操作,但它更友好。
建立一個nametuple
1 |
TestResults = namedTuple('TestResults', ['failed', 'attempted']) |
unpack序列
1 2 3 4 5 6 7 |
p = 'Raymond', 'Hettinger', 0x30, 'python@example.com' # 其它語言的常用方法/習慣 fname = p[0] lname = p[1] age = p[2] email = p[3] |
更好的方法
1 |
fname, lname, age, email = p |
第二種方法用了unpack元組,更快,可讀性更好。
更新多個變數的狀態
1 2 3 4 5 6 7 8 |
def fibonacci(n): x = 0 y = 1 for i in range(n): print x t = y y = x + y x = t |
更好的方法
1 2 3 4 5 |
def fibonacci(n): x, y = 0, 1 for i in range(n): print x x, y = y, x + y |
第一種方法的問題
- x和y是狀態,狀態應該在一次操作中更新,分幾行的話狀態會互相對不上,這經常是bug的源頭。
- 操作有順序要求
- 太底層太細節
第二種方法抽象層級更高,沒有操作順序出錯的風險而且更效率更高。
同時狀態更新
1 2 3 4 5 6 7 8 |
tmp_x = x + dx * t tmp_y = y + dy * t tmp_dx = influence(m, x, y, dx, dy, partial='x') tmp_dy = influence(m, x, y, dx, dy, partial='y') x = tmp_x y = tmp_y dx = tmp_dx dy = tmp_dy |
更好的方法
1 2 3 4 |
x, y, dx, dy = (x + dx * t, y + dy * t, influence(m, x, y, dx, dy, partial='x'), influence(m, x, y, dx, dy, partial='y')) |
效率
- 優化的基本原則
- 除非必要,別無故移動資料
- 稍微注意一下用線性的操作取代O(n**2)的操作
總的來說,不要無故移動資料
連線字串
1 2 3 4 5 6 7 |
names = ['raymond', 'rachel', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie'] s = names[0] for name in names[1:]: s += ', ' + name print s |
更好的方法
1 |
print ', '.join(names) |
更新序列
1 2 3 4 5 6 7 |
names = ['raymond', 'rachel', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie'] del names[0] # 下面的程式碼標誌著你用錯了資料結構 names.pop(0) names.insert(0, 'mark') |
更好的方法
1 2 3 4 5 6 7 |
names = deque(['raymond', 'rachel', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie']) # 用deque更有效率 del names[0] names.popleft() names.appendleft('mark') |
裝飾器和上下文管理
- 用於把業務和管理的邏輯分開
- 分解程式碼和提高程式碼重用性的乾淨優雅的好工具
- 起個好名字很關鍵
- 記住蜘蛛俠的格言:能力越大,責任越大
使用裝飾器分離出管理邏輯
1 2 3 4 5 6 7 |
# 混著業務和管理邏輯,無法重用 def web_lookup(url, saved={}): if url in saved: return saved[url] page = urllib.urlopen(url).read() saved[url] = page return page |
更好的方法
1 2 3 |
<a href="http://www.jobbole.com/members/jjq153287083">@cache</a> def web_lookup(url): return urllib.urlopen(url).read() |
注意:Python 3.2開始加入了functools.lru_cache解決這個問題。
分離臨時上下文
1 2 3 4 5 |
# 儲存舊的,建立新的 old_context = getcontext().copy() getcontext().prec = 50 print Decimal(355) / Decimal(113) setcontext(old_context) |
更好的方法
1 2 |
with localcontext(Context(prec=50)): print Decimal(355) / Decimal(113) |
譯註:示例程式碼在使用標準庫decimal,這個庫已經實現好了localcontext。
如何開啟關閉檔案
1 2 3 4 5 |
f = open('data.txt') try: data = f.read() finally: f.close() |
更好的方法
1 2 |
with open('data.txt') as f: data = f.read() |
如何使用鎖
1 2 3 4 5 6 7 8 9 10 |
# 建立鎖 lock = threading.Lock() # 使用鎖的老方法 lock.acquire() try: print 'Critical section 1' print 'Critical section 2' finally: lock.release() |
更好的方法
1 2 3 4 |
# 使用鎖的新方法 with lock: print 'Critical section 1' print 'Critical section 2' |
分離出臨時的上下文
1 2 3 4 |
try: os.remove('somefile.tmp') except OSError: pass |
更好的方法
1 2 |
with ignored(OSError): os.remove('somefile.tmp') |
ignored是Python 3.4加入的, 文件。
注意:ignored 實際上在標準庫叫suppress(譯註:contextlib.supress).
試試建立你自己的ignored上下文管理器。
1 2 3 4 5 6 |
@contextmanager def ignored(*exceptions): try: yield except exceptions: pass |
把它放在你的工具目錄,你也可以忽略異常
譯註:contextmanager在標準庫contextlib中,通過裝飾生成器函式,省去用__enter__和__exit__寫上下文管理器。詳情請看文件。
分離臨時上下文
1 2 3 4 5 6 7 8 |
# 臨時把標準輸出重定向到一個檔案,然後再恢復正常 with open('help.txt', 'w') as f: oldstdout = sys.stdout sys.stdout = f try: help(pow) finally: sys.stdout = oldstdout |
更好的寫法
1 2 3 |
with open('help.txt', 'w') as f: with redirect_stdout(f): help(pow) |
redirect_stdout在Python 3.4加入(譯註:contextlib.redirect_stdout), bug反饋。
實現你自己的redirect_stdout上下文管理器。
1 2 3 4 5 6 7 8 |
@contextmanager def redirect_stdout(fileobj): oldstdout = sys.stdout sys.stdout = fileobj try: yield fieldobj finally: sys.stdout = oldstdout |
簡潔的單句表達
兩個衝突的原則:
- 一行不要有太多邏輯
- 不要把單一的想法拆分成多個部分
Raymond的原則:
- 一行程式碼的邏輯等價於一句自然語言
列表解析和生成器
1 2 3 4 5 |
result = [] for i in range(10): s = i ** 2 result.append(s) print sum(result) |
更好的方法
1 |
print sum(i**2 for i in xrange(10)) |
第一種方法說的是你在做什麼,第二種方法說的是你想要什麼。