譯序python
若是說優雅也有缺點的話,那就是你須要艱鉅的工做才能獲得它,須要良好的教育才能欣賞它。web
—— 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的代碼而困擾,或許這份筆記能給你幫助。app
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中加入。高效而優雅。
提升可讀性
用關鍵字參數提升函數調用的可讀性
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
第一種方法的問題
第二種方法抽象層級更高,沒有操做順序出錯的風險並且更效率更高。
同時狀態更新
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'))
效率
總的來講,不要無端移動數據
鏈接字符串
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')
裝飾器和上下文管理
使用裝飾器分離出管理邏輯
# 混着業務和管理邏輯,沒法重用
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
簡潔的單句表達
兩個衝突的原則:
Raymond的原則:
列表解析和生成器
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開發者