Python技巧(漂亮又通順的程式碼)

##Looping over a range of numbers

你有可能這麼寫:

for i in [0, 1, ,2, 3, 4, 5]:
    print i**2

比較有效率的寫法, 使用range():

for i in range(6):
    print i**2

由於range()會傳回list, 所以當數量很大時, 最好使用xrange(), 以減少記憶體的使用量(xrange在Python 3之後已經拿掉, 原來的range改成傳回疊代).

for i in xrange(6):
    print i**2

##Looping over a collection

當要依序取得一個list內項目的值時:

colors = ['red', 'green', 'blue', 'yellow']

用C語言的思維, 你可能會寫這樣:

for i in range(len(colors)):
    print(colors[i])

然而, 在Python語言, 可以直接用for迴圈來跑過list的項目:

for color in colors:
    print(color)

##Looping backwards

上例是依正順序取得list的項目, 但如果要反向順序來取得的話呢? 在C語言的思維, 可能會寫這樣:

for i in range(len(colors), -1, -1):
    print(colors[i])

在Python中, 直接用reversed就可以得到反順序的list:

for color in reversed(colors):
    print(color)

##Looping over a collection and indices

當有一個list, 我們要依序得到項目的次序以及值的時候, 我們可能會寫這樣:

for i in range(len(colors)):
    print(i, '-->', colors[i])

其實在Python中, 我們可以使用enumerate(), 它會傳回一個次序及值成組tuple的疊代:

for i, color in enumerate(colors):
    print(i, '-->', color)

##Looping over two collections

當我們要對兩個list依序對其值做對應的時候, 用C語言的思維, 我們可能會寫這樣:

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

而Python中, 我們使用zip()就可以得到想要的結果:

for name, color in zip(names, colors):
    print(name, '-->', color)

然而, 用zip()會傳回一個新的list, 也就是會需要使用額外的記憶體. 為此, 我們可以使用izip():

from itertools import izip
for name, color in izip(names, colors):
    print(name, '-->', color)

##Looping in sorted order

在Python中, 我們很輕易就可以得到一個排序過的list: 使用sorted(). sorted()會傳回一個新的list, 並不會改動到原來的list.

colors = ['red', 'green', 'blue', 'yellow']
for color in sorted(colors):
    print(color)

要反向的排序:

for color in sorted(colors, reverse=True):
    print(color)

##Custom sort oder

承上例, 如果要做自訂的排序規則, 例如用字串的長度來排序, 在傳統Python的作法, 我們可能會這樣寫:

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)

這樣的寫法, 我們要寫一個比較用的function, 它會將list的值兩兩比對, 回傳大於0, 小於0, 或等於0的值讓sorted()來做排序.

其實, 還有一個更簡便的方式:

print(sorted(colors, key=len))

使用key, 它會將list的項目用key的函式來計算出值(在此例是len()), 再依此值來排序. 又快又簡單.

cmp在Python 3已經被拿掉, 請用key這個又快又簡單的方式.

##Call a function until a sentinel value

一直呼叫一個函式(function)直到遇到某個停止的值. 這個情況常發生在讀取檔案的時候. 如:

blocks = []
while True:
    block = f.read(32)
    if block = '':
        break
    blocks.append(block)

我們可以利用iter()以及partial()來達成, 程式碼會變得簡潔.

from functools import partial
blocks = []
for block in iter(partial(f.read, 32), ''):
    blocks.append(block)

在這個例子中, iter()會一直呼叫f.read(32), 並將回傳值産生疊代, 直到回傳值是空字串”.

iter(callback, sentinel)第一個參數是function, 它會一直呼叫這個function, 將function的回傳值變成疊代, 直到function回傳值與sentinel相等.

partial(func, *args, **keywords)産生一個新的function, 而這個function是將參數args或keywords代入func的結果. 參數可以是func所需要的部份參數.

舉例如下:

from functools import partial

def add(a, b):
    return a+b

add_by_2 = partial(add, b=2)
print(add_by_2(3))

上述的add_by_2是一個partial()新産生的function, 它利用add(a, b)而將b的值固定為2. 所以呼叫add_by_2(3)會得到5.

##Distinguishing multiple exit points in loop

在for迴圈中達成某條件就跳出迴圈, 例如用for迴圈找list是否有某項值, 找到回傳1, 沒找到回傳-1.

一般情況會寫這樣:

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

Python的for迴圈其實可以搭配else使用, 其實應該稱作no-break. 與for使用的else表示如果for迴圈被完整執行, 那就執行else的部份. 如果迴圈被中斷了(break), 那else的部份也不會被執行.

所以上述的程式可以改寫成:

def find(seq, target):
    for i, value in enumerate(seq):
        if value == target:
            break
    else:
        return -1
    return i

##Looping over dictionary keys

在dictionary中, 我們要得到key值, 可以用下例的方式:

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}
for k in d:
    print(k)

然而, 如果會變動到dictionary (如刪除某些dictionary的項目), 應該使用d.keys(), 這個函式會産生一個由key組成的list.

for k in d.keys():
    if k.startswith('r'):
        del d[k]

當然也可以用下例的程式得到相同的結果.

d = {k:d[k] for k in d if not k.startswith('r')}

這行程式的意思是, d重設為一個新的dictionary, 而這個新的dictionary內容是k:d[k]的組合, 而k是d的key但不是’r’開頭的.

##Looping over a dictionary keys and values

承上例, 要得到dictionary的key值可以用如下的程式碼:

for k in d:
    print(k)

如果要連同value一起取得, 可以用d.items(), 這會産生一個由(key, value)的tuple組成的list.

for k, v in d.items():
    print(k, '-->', v)

相同的, 産生一個新的list會佔用額外的記憶體, 所以如果只是要疊代, 可以用d.iteritems():

for k, v in d.iteritems():
    print(k, '-->', v)

##Construct a dictionary from pairs

如果我們有兩個lists, 想要把它們變成key:value的dictionary, 我們可以這樣寫:

names = ['raymond', 'rachel', 'matthew']
colors = ['red', 'green', 'blue']

from itertools import izip
d = dict(izip(names, colors))

{'raymond':'red', 'rachel': 'green', 'matthew':'blue'}

d = dict(enumerate(names))
{0:'raymond', 1:'rachel', 2:'matthew'}

##Counting with dictionaries

假設我們有一個list, 裡面有很多項目, 而我們要計數每個項目重複的數量有多少, 我們會這樣寫:

colors = ['red', 'green', 'red', 'blue', 'green', 'red']
d = {}
for color in colors:
    if not color in d:
        d[color] = 0
    d[color] += 1

{'blue': 1, 'green': 2, 'red': 3}

這個程式的意思是, 先設d為一個空的dictionary, 用for迴圈跑過所有colors的項目, 如果color還沒有在d的key當中, 就把d[color]設為0, 如果有color這個key, 就把d[color]加1.

上述的程式是要先確認color是否存在d的key當中, 如果沒有, 就直接用d[color]來讀取會發生錯誤例外.

我們也可以改用d.get()來得到相同的結果:

d = {} 
for color in colors: 
d[color] = d.get(color, 0) + 1

d.get()的第二個參數是預設值, 也就是如果color這個key找不到的話, 就回傳預設值.

另外, 也可以用defaultdict來實作:

from collections import defaultdict
d = defaultdict(int)
for color in colors:
    d[color] += 1

defaultdict(default_factory)會産生一個以default_factory為基本的dictionary, 也就是key不需要存在這個dictionary當中, 它的d[key]值便會是default_factory這個type的預設值(int為0).

##Grouping with dictionary

假設我們需要將list的項目, 依照它們的長度來群組起來, 我們可以這樣寫:

names = ['raymond', 'rachel', 'matthew', 'roger', 'betty', 'melissa', 'judith', 'charlie']
d = {}
for name in names:
    key = len(name)
    if not key in d:
        d[key] = []
    d[key].append(name)

{5:['roger', 'betty'], 6:['rachel', 'judith'], 7:['raymond', 'matthew', 'melissa', 'charlie']}

這段程式的意思是, 設d為一個空的dictionary, 用for跑過所有的name, 把key設為name的長度. 如果key不在d的keys中, 就把d[key]設為一個空的list, 然後把name加到d[key]這個list中.

更好的方式:

d = {}
for name in names:
    key = len(name)
    d.setdefault(key, []).append(name)

d.setdefault(key, [])就類似是d.get(key, []), 但跟get()不同的是, setdefault()如果找不到key, 就將d[key]設為預設值(此例是[]), 而get()是找不到key,就回傳預設值.

當然, 也可以用之前提過的defaultdict來實作:

from collections import defaultdict
d = defaultdict(list)
for name in names:
    key = len(name)
    d[key].append(name)

##Is a dictionary popitem() atomic?

d.popitem()會刪掉(key, value)並回傳該tuple的函式. 用法如:

d = {'matthew': 'blue', 'rachel': 'green', 'raymond': 'red'}
while d:
    key, value = d.popitem()
    print(key, '-->', value)

而d.popitem()是atomic, 所以在thread的使用中, 不需要在前後做lock.

##Linking dictionaries

假設我們有一堆的設定值存在dictionary中, 有一些是程式預設, 有一些在環境設定裡, 有一些是使用者輸入來設定. 我們作法可能如:

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)

但是如果dictionary的key/value數量很多的話, copy()/update()會非常沒有效率而緩慢.

在Python 3.3加入了ChainMap來達成這個目的.

from collections import ChainMap
d = ChainMap(command_line_args, os.environ, defaults)

书籍推荐