譯序
如果說優雅也有缺點的話,那就是你需要艱巨的工作才能得到它,需要良好的教育才能欣賞它。
—— 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的演講。這是我按我的理解整理出來的,希望你們理解起來跟我一樣順暢!
遍歷一個范圍內的數字
for ?i? in ?[0,?1,?2,?3,?4,?5]:
????print?i ** 2
for ?i? in ?range(6):
????print?i ** 2
更好的方法
for ?i? in ?xrange(6):
????print?i ** 2
xrange會返回一個迭代器,用來一次一個值地遍歷一個范圍。這種方式會比range更省內存。xrange在Python 3中已經改名為range。
遍歷一個集合
colors?= ['red',?'green',?'blue',?'yellow']
for ?i? in ?range(len(colors)):
????print colors[i]
更好的方法
for ?color in ?colors:
????print color
反向遍歷
colors?= ['red',?'green',?'blue',?'yellow']
for ?i? in ?range(len(colors)-1,?-1,?-1):
????print colors[i]
更好的方法
for ?color in ?reversed(colors):
????print color
遍歷一個集合及其下標
colors?= ['red',?'green',?'blue',?'yellow']
for ?i? in ?range(len(colors)):
????print?i,?'--->',?colors[i]
更好的方法
for ?i,?color in ?enumerate(colors):
????print?i,?'--->',?color
這種寫法效率高,優雅,而且幫你省去親自創建和自增下標。
當你發現你在操作集合的下標時,你很有可能在做錯事。
遍歷兩個集合
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
更好的方法
for ?name,?color in ?izip(names,?colors):
????print name,?'--->',?color
zip在內存中生成一個新的列表,需要更多的內存。izip比zip效率更高。
注意:在Python 3中,izip改名為zip,并替換了原來的zip成為內置函數。
有序地遍歷
colors?= ['red',?'green',?'blue',?'yellow']
# 正序
for ?color in ?sorted(colors):
????print colors
# 倒序
for ?color in ?sorted(colors,?reverse= True ):
????print colors
自定義排序順序
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)
更好的方法
print sorted(colors, key=len)
第一種方法效率低而且寫起來很不爽。另外,Python 3已經不支持比較函數了。
調用一個函數直到遇到標記值
blocks?= []
while ? True :
????block?= f.read(32)
???? if ?block?== '':
???????? break
????blocks.append(block)
更好的方法
blocks?= []
for ?block in ?iter(partial(f.read,?32),?''):
????blocks.append(block)
iter接受兩個參數。第一個是你反復調用的函數,第二個是標記值。
譯注:這個例子里不太能看出來方法二的優勢,甚至覺得partial讓代碼可讀性更差了。方法二的優勢在于iter的返回值是個迭代器,迭代器能用在各種地方,set,sorted,min,max,heapq,sum……
在循環內識別多個退出點
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
更好的方法
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
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
# 并不快,每次必須要重新哈希并做一次查找
for ?k? in ?d:
????print?k,?'--->',?d[k]
# 產生一個很大的列表
for ?k,?v? in ?d.items():
????print?k,?'--->',?v
更好的方法
for ?k,?v? in ?d.iteritems():
????print?k,?'--->',?v
iteritems()更好是因為它返回了一個迭代器。
注意:Python 3已經沒有iteritems()了,items()的行為和iteritems()很接近。詳情請看文檔。
用 key-value 對構建字典
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))
用字典計數
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}
更好的方法
d?= {}
for ?color in ?colors:
????d[color]?= d.get(color,?0)?+ 1
# 稍微潮點的方法,但有些坑需要注意,適合熟練的老手。
d?= defaultdict( int )
for ?color in ?colors:
????d[color]?+= 1
用字典分組 ?— 第 I 部分和第 II 部分
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)
更好的方法
d?= defaultdict(list)
for ?name in ?names:
????key?= len(name)
????d[key].append(name)
字典的 popitem() 是原子的嗎?
d?= {'matthew': 'blue',?'rachel': 'green',?'raymond': 'red'}
while ?d:
????key,?value?= d.popitem()
????print key,?'-->',?value
popitem是原子的,所以多線程的時候沒必要用鎖包著它。
連接字典
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)
更好的方法
d = ChainMap(command_line_args, os.environ, defaults)
ChainMap在Python 3中加入。高效而優雅。
提高可讀性
[if !supportLists]·?[endif]位置參數和下標很漂亮
[if !supportLists]·?[endif]但關鍵字和名稱更好
[if !supportLists]·?[endif]第一種方法對計算機來說很便利
[if !supportLists]·?[endif]第二種方法和人類思考方式一致
用關鍵字參數提高函數調用的可讀性
twitter_search('@obama', False, 20, True)
更好的方法
twitter_search('@obama', retweets=False, numtweets=20, popular=True)
第二種方法稍微(微秒級)慢一點,但為了代碼的可讀性和開發時間,值得。
用 namedtuple 提高多個返回值的可讀性
# 老的testmod返回值
doctest.testmod()
# (0, 4)
# 測試結果是好是壞?你看不出來,因為返回值不清晰。
更好的方法
# 新的testmod返回值, 一個namedtuple
doctest.testmod()
# TestResults(failed=0, attempted=4)
namedtuple是tuple的子類,所以仍適用正常的元組操作,但它更友好。
創建一個nametuple
TestResults = namedTuple('TestResults', ['failed', 'attempted'])
unpack 序列
p?= 'Raymond',?'Hettinger',?0x30,?'python@example.com'
# 其它語言的常用方法/習慣
fname?= p[0]
lname?= p[1]
age?= p[2]
email?= p[3]
更好的方法
fname, lname, age, email = p
第二種方法用了unpack元組,更快,可讀性更好。
更新多個變量的狀態
def fibonacci(n):
????x?= 0
????y?= 1
???? for ?i? in ?range(n):
????????print?x
????????t?= y
????????y?= x?+ y
????????x?= t
更好的方法
def fibonacci(n):
????x,?y?= 0,?1
???? for ?i? in ?range(n):
????????print?x
????????x,?y?= y,?x?+ y
第一種方法的問題
[if !supportLists]·?[endif]x和y是狀態,狀態應該在一次操作中更新,分幾行的話狀態會互相對不上,這經常是bug的源頭。
[if !supportLists]·?[endif]操作有順序要求
[if !supportLists]·?[endif]太底層太細節
第二種方法抽象層級更高,沒有操作順序出錯的風險而且更效率更高。
同時狀態更新
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
更好的方法
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'))
效率
[if !supportLists]·?[endif]優化的基本原則
[if !supportLists]·?[endif]除非必要,別無故移動數據
[if !supportLists]·?[endif]稍微注意一下用線性的操作取代O(n**2)的操作
總的來說,不要無故移動數據
連接字符串
names?= ['raymond',?'rachel',?'matthew',?'roger',
???????? 'betty',?'melissa',?'judith',?'charlie']
s?= names[0]
for ?name in ?names[1:]:
????s?+= ', '?+ name
print?s
更好的方法
print ', '.join(names)
更新序列
names?= ['raymond',?'rachel',?'matthew',?'roger',
???????? 'betty',?'melissa',?'judith',?'charlie']
del names[0]
# 下面的代碼標志著你用錯了數據結構
names.pop(0)
names.insert(0,?'mark')
更好的方法
names?= deque(['raymond',?'rachel',?'matthew',?'roger',
?????????????? 'betty',?'melissa',?'judith',?'charlie'])
# 用deque更有效率
del names[0]
names.popleft()
names.appendleft('mark')
裝飾器和上下文管理
[if !supportLists]·?[endif]用于把業務和管理的邏輯分開
[if !supportLists]·?[endif]分解代碼和提高代碼重用性的干凈優雅的好工具
[if !supportLists]·?[endif]起個好名字很關鍵
[if !supportLists]·?[endif]記住蜘蛛俠的格言:能力越大,責任越大
使用裝飾器分離出管理邏輯
# 混著業務和管理邏輯,無法重用
def web_lookup(url,?saved={}):
???? if ?url in ?saved:
???????? return ?saved[url]
????page?= urllib.urlopen(url).read()
????saved[url]?= page
???? return ?page
更好的方法
@cache
def web_lookup(url):
???? return ?urllib.urlopen(url).read()
注意:Python 3.2開始加入了functools.lru_cache解決這個問題。
分離臨時上下文
# 保存舊的,創建新的
old_context?= getcontext().copy()
getcontext().prec?= 50
print Decimal(355)?/ Decimal(113)
setcontext(old_context)
更好的方法
with localcontext(Context(prec=50)):
????print Decimal(355)?/ Decimal(113)
譯注:示例代碼在使用標準庫decimal,這個庫已經實現好了localcontext。
如何打開關閉文件
f?= open('data.txt')
try :
????data?= f.read()
finally :
????f.close()
更好的方法
with open('data.txt')? as ?f:
????data?= f.read()
如何使用鎖
# 創建鎖
lock?= threading.Lock()
# 使用鎖的老方法
lock.acquire()
try :
????print?'Critical section 1'
????print?'Critical section 2'
finally :
????lock.release()
更好的方法
# 使用鎖的新方法
with lock:
????print?'Critical section 1'
????print?'Critical section 2'
分離出臨時的上下文
try :
????os.remove('somefile.tmp')
except OSError:
????pass
更好的方法
with ignored(OSError):
????os.remove('somefile.tmp')
ignored是Python 3.4加入的, 文檔。
注意:ignored 實際上在標準庫叫suppress(譯注:contextlib.supress).
試試創建你自己的 ignored 上下文管理器。
@contextmanager
def ignored(*exceptions):
???? try :
????????yield
????except exceptions:
????????pass
把它放在你的工具目錄,你也可以忽略異常
譯注:contextmanager在標準庫contextlib中,通過裝飾生成器函數,省去用__enter__和__exit__寫上下文管理器。詳情請看文檔。
分離臨時上下文
# 臨時把標準輸出重定向到一個文件,然后再恢復正常
with open('help.txt',?'w')? as ?f:
????oldstdout?= sys.stdout
????sys.stdout?= f
???? try :
????????help(pow)
???? finally :
????????sys.stdout?= oldstdout
更好的寫法
with open('help.txt',?'w')? as ?f:
????with redirect_stdout(f):
????????help(pow)
redirect_stdout在Python 3.4加入(譯注:contextlib.redirect_stdout),?bug反饋。
實現你自己的 redirect_stdout 上下文管理器。
@contextmanager
def redirect_stdout(fileobj):
????oldstdout?= sys.stdout
????sys.stdout?= fileobj
???? try :
????????yield fieldobj
???? finally :
????????sys.stdout?= oldstdout
簡潔的單句表達
兩個沖突的原則:
[if !supportLists]·?[endif]一行不要有太多邏輯
[if !supportLists]·?[endif]不要把單一的想法拆分成多個部分
Raymond 的原則:
[if !supportLists]·?[endif]一行代碼的邏輯等價于一句自然語言
列表解析和生成器
result?= []
for ?i? in ?range(10):
s?= i ** 2
????result.append(s)
print sum(result)
更好的方法
print sum(i**2 for i in xrange(10))
第一種方法說的是你在做什么,第二種方法說的是你想要什么。
編譯:0xFEE1C001?
www.lightxue.com/transforming-code-into-beautiful-idiomatic-python
來源:Python開發者
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
