讓你的Python代碼優雅又地道

譯序

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

—— 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中加入。高效而優雅。

提高可讀性

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

用關鍵字參數提高函數調用的可讀性

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, '[email protected]'
 

其它語言的常用方法/習慣

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

第一種方法的問題

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

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

同時狀態更新

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'))

效率

優化的基本原則
除非必要,別無故移動數據
稍微注意一下用線性的操作取代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')

裝飾器和上下文管理

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

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

混着業務和管理邏輯,無法重用

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開發者

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章