說來慚愧,用了這麼久的 Python,寫出來的 code 跟別人感覺就是差很多。又很懶得去看英文資料 (是有沒有這麼懶阿 Orz)
幸好發現了一位前輩整理的 Python 慣用語系列文


以下內容皆轉載於: http://seanlin.logdown.com/
如有需要轉載者,請詢問原作者


目錄

If Statements

  1. If Statements
  2. In
  3. Conditional Expressions
  4. Chained Comparisons
  5. Loop else

Tupple, List

  1. Tuple
  2. Enumerate
  3. 負數索引值
  4. Join 生成字串
  5. List Comprehensions
  6. Generator Expressions

Dictionary

  1. dict.get()
  2. dict.setdefault()
  3. defaultdict

Parameter, Function

  1. 避免用 mutable 預設引數
  2. 用 _ 代表未使用的變數
  3. Property

Other

  1. BIFs
  2. Context Managers
  3. 太長怎麼辦

If Statements

在 PEP 8 有提到,別和 True 以及 False 用 == 比較

慣用

if valid():
    print 'valid'
if not valid():
    print 'invalid'

非慣用

if valid() == True:
    print 'valid'
if valid() == False:
    print 'invalid'

sequences (strings、lists、 tuples) 來說,因為空的 sequence 是 False,不需判斷長度是否為零

慣用

if not users:
    print 'No users available'

非慣用

if len(users) == 0:
    print 'No users available'

對數值形態比較時需要明確地比值

if i % 2 == 0:
    print 'even number'

非慣用

if not i % 2:
    print 'even number'

和 None 比較要使用 is 和 is not,儘管在正常情況下和 == 沒有啥差別,但這已經是大家習慣的用法了,以下來自 PEP8。

Comparisons to singletons like None should always be done with is or is not, never the equality operators.

在 stackoverflow 有人舉例

class Negator(object):
    def __eq__(self, other):
        return not other

thing = Negator()
print thing == None    # True

print thing is None    # False

Back to Outline


In

if statements

你覺得哪種寫法較清楚呢?

慣用

if name in ('Alice', 'Bob', 'Charlie'):
    print 'ok'

非慣用

if name == 'Alice' or name == 'Bob' or name == 'Charlie':
    print 'ok'

替代 find()

如果只是想知道某字串是否包含特定字串

慣用

msg = 'Hello world!'
if 'world' in msg:
    print 'find it' 

非慣用

msg = 'Hello world!'
if msg.find('world') != -1:
    print 'find it' 

Back to Outline


Conditional Expressions

許多程式語言都有 ternary operator ?: 這種東西,Python 並沒有這樣的 operator ,但有個慣用表達方式可以達到類似目的,不過要注意太複雜的語句就不適合這樣使用了。

慣用

 def foo(logging):
    level = (1 if logging else 0)

非慣用

 def foo(logging):
    if logging:
        level = 1
    else:
        level = 0

注意等號右邊使用括號括起來,這是比較推薦的寫法,不過許多程式都沒採用這樣的寫法。

Back to Outline


Chained Comparisons

Python 的比較運算子可以練接起來,這是許多語言沒有的特性,正常情況下可讓程式更符合實際上的意義,提高可讀性。

慣用

if a < b < c <= d:
    return True

非慣用

if a < b and b < c and c <= d:
    return True

不正常的情況就是濫用此一特性,寫出像這樣的東西

if data == result is not None:
    return True

Back to Outline


Loop Else

Python 中 for 以及 while loop 可以像 if 一樣有個 else 區塊,當迴圈沒有中斷就會執行到 else 區塊

Python tutorial:

Loop statements may have an else clause; it is executed when the loop terminates through exhaustion of the list (with for) or when the condition becomes false (with while), but not when the loop is terminated by a break statement.

這乍看之下沒有甚麼用,但在一些情況下是有好處的,可以取代大家常用的 flag 這種暫時性的變數,讓程式的意圖更為明顯。

 for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print n, 'equals', x, '*', n/x
            break
    else:
        print n, 'is a prime number'

可以取代下面的寫法

 for n in range(2, 10):
    is_prime = True
    for x in range(2, n):
        if n % x == 0:
            print n, 'equals', x, '*', n/x
            is_prime = False
            break
    if is_prime:
        print n, 'is a prime number'

Back to Outline


Tuple 的妙用

Tuple Packing/Unpacking

tuple packing/unpacking 是什麼呢?

t = 12345, 54321, 'hello!'
x, y, z = t

第一個是 tuple packing ,等號右邊被打包成 tuple ,第二個是 tuple unpacking ,就是反過來,其實不只 tuple 可這樣用,詳情請看 Python tutorial 的說明。

Multiple Assignment

慣用

my_list = ['Alice', 12, 'Python']
(name, age, skill) = my_list

非慣用

my_list = ['Alice', 12, 'Python']
name = my_list[0]
age = my_list[1]
skill = my_list[2]

Swap

在其他語言,例如 C,要交換兩個變數通常是使用第三個變數 temp ,對數值型態的話有不同的邪惡技巧,但可讀性不佳。在 Python 利用 tuple 可達成目的,而且不需要暫時變數。

(x, y) = (y, x)

回傳多值

tuple 還有其他應用,知道 Python 函式怎麼回傳多值嗎?

def get_error_details():
    return (2, 'details')

(errnum, errstr) = get_error_details()

Back to Outline


Enumerate

從其他語言陣營來的人可能很習慣對陣列這麼操作:

for (int i = 0; i < a.size(); i++)
{
    // print i, a[i] ...
}

也就是直接拿個變數當 index 來操作,在 Python 中如果需要用到 index 的話,可使用內建函式 enumerate

慣用

names = ['Alice', 'Bob', 'Cindy']
for index, element in enumerate(names):
    print '%d %s' % (index, element)

非慣用

names = ['Alice', 'Bob', 'Cindy']
index = 0
while index < len(names):
    print '%d %s' % (index, names[index])
    index += 1

Back to Outline


負數索引值

Python 的 index 可以是負數,這對許多人來說是比較不習慣的特性。例如想得到字串的最後一個字

慣用

word[-1]

非慣用

word_len = len(word)
word[word_len - 1]

也可以用於 slicing ,以下程式傳回帳號末四碼

慣用

def get_check_number(account):
    return account[-4:]

非慣用

def get_check_number(account):
    account_len = len(account)
    return account[account_len - 4:]

Back to Outline


Join 生成字串

當你要從一個 list 裡頭的一堆字串串接起來時,Python 有個高效率且可讀性高的做法。

name_list = ['Alice', 'Bob', 'Cindy']
name_formatted = ', '.join(name_list)

Back to Outline


List Comprehensions

Python 2.7 以後有三種 comprehensions,在此之前只有 list comprehensions

  • list comprehensions
  • dictionary comprehensions
  • set comprehensions

其中 list comprehensions 是最常見到的,使用時機是當你需要從 iterable 製造出另一個 list 時,適當地使用可以讓程式碼更清楚,而且比起 map() 以及 filter() 來得更簡單且有彈性。所謂的「適當地使用」要看你所處的團隊怎麼定義,例如 Google Python style guide 不建議超過一個 for 和 filter。

慣用

import glob
import os

imgs = [f.upper() for f in glob.iglob('*.gif') if os.stat(f).st_size > 2000]

非慣用

import glob
import os

imgs = []
for f in glob.iglob('*.gif'):
    if os.stat(f).st_size > 2000:
        imgs.append(f.upper())

也可使用 filter()map(),但可讀性比較差。

import glob
import os

imgs = map(lambda x: x.upper(),
           filter(lambda f: os.stat(f).st_size > 2000, glob.iglob('*.gif'))

Back to Outline


Generator Expressions

之前提到的 list comprehensions 是很強大的東西,但有時我們並不需要用到一個完整的 list,這時就可使用 generator expressions。

Generator expressions 跟 list comprehensions 結構很像,只差在它是以 () 包起來,而不是 []。簡單講就是 generator 版的 list comprehensions,不同之處在於它不會建立 list ,而是 generator 物件。如此一來好處就是節省記憶體空間,尤其是你要處理的數量很巨大時。

如果 generator expressions 做為函式唯一參數時,可以省掉括號,但其他情況下括號不可省略。

# http://legacy.python.org/dev/peps/pep-0289/


sum(x**2 for x in range(10))
reduce(operator.add, (x**2 for x in range(10)))
g = (x**2 for x in range(10))

下面的程式碼來自 A Nice Little Bit of Python,用途是檢查字串是否有特定的結尾。

慣用

if any(needle.endswith(e) for e in ('ly', 'ed', 'ing', 'ers')):
    print('Is valid')
else:
    print('Invalid')

非慣用

if any([needle.endswith(e) for e in ('ly', 'ed', 'ing', 'ers')]):
    print('Is valid')
else:
    print('Invalid')

Back to Outline


Dict.get

善用 dict.get() 的 default,可以幫助我們寫出更精巧且可讀性更好的程式。

慣用

name = some_dict.get('name', 'Bob')

非慣用

if 'name' in some_dict:
    name = some_dict['name']
else:
    name = 'Bob'

上面可以再進一步改成這樣:

name = (some_dict['name'] if 'name' in some_dict else 'Bob')

但是不如 dict.get() 來得好。

Back to Outline


dict.setdefault()

dict.setdefault(key, default=None)

dict.setdefault()dict.get() 類似,但不一樣的地方在於,如果 key 不在該 dictionary 中,就會設 dict[key] = default,然後回傳 default ,如果 key 存在的話,就回傳 dict[key]

善用 dict.setdefault() 可以幫助我們簡化程式,下面的例子中使用它可以省掉判斷 key 存不存在的程式碼。

the_dict = {}
for author, book in books:
    the_dict.setdefault(author, []).append(book)

Back to Outline


defaultdict

dict.setdefault() 的用法,但有可讀性更高的做法,但不是使用內建的 dict ,而是 collections 裡的 defaultdict。

defaultdict 是在 Python 2.5 正式加入的,所以較早期的程式碼看不到它的身影,我們來看一下怎麼達到 dict.setdefault() 的程式:

import collections

the_dict = collections.defaultdict(list)
for author, book in books:
    the_dict[author].append(book)

defaultdict 的第一個引數是一個 factory function ,用來替 defaultdict 實例裡頭不存在的 key 設定 value 預設值,這上面的例子中為 list ,所以每個預設值為 list(),也就是 []

defaultdict 其餘的用法和 dict 是一樣的,因為是 dict 的子類。

Back to Outline


避免用 mutable 預設引數

Python tutorial:

Important warning: The default value is evaluated only once. This makes a difference when the default is a mutable object such as a list, dictionary, or instances of most classes.

會有什麼風險請參閱 Python tutorial 裡面的例子,常用的解決方式是使用 None 當預設引數。

慣用

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

有風險

def f(a, L=[]):
    L.append(a)
    return L

注意這並不是 Python 的 bug,而是它的一種特性,Python 裡頭凡事皆物件,所以函式也不例外。函式有個 attribute 叫做 defaults 拿來存放預設引數,你可以做個小實驗,印出上面的 f.defaults,結果會是一個 1-tuple,裡面放的是一個 list。

我們可以利用此一特性來做 memoization,例如用 DP 求 Fibonacci number:

def fib(n, memo={}):
    if n < 2:
        return 1
    if n not in memo:
        memo[n] = fib(n-1) + fib(n-2)
    return memo[n]

Back to Outline


用 _ 代表未使用的變數

當使用回傳 tuple 的函式時,有時候不是每一個值都需要用到,這時我們習慣上使用 _ 作為暫時變數的名稱。不過你的程式如果須支援 i18n ,慣例上也是用 _ 當 gettext 的簡寫,例如在 Django 通常是這麼寫的:

from django.db import models
from django.utils.translation import ugettext_lazy as _


class MyThing(models.Model):
    name = models.CharField(help_text=_('This is the help text'))

這時就會跟我們的 _ 有衝突,所以有需要的話可改用 __ 來解決這個問題。

慣用

filename = 'foobar.txt'
basename, _, ext = filename.rpartition('.')

非慣用

filename = 'foobar.txt'
basename, tmp, ext = filename.rpartition('.')

也可在 iteration 使用

for _ in range(10):
    print 'Hello World!'

Back to Outline


Property

寫 Java 的人習慣寫一堆 getX() 以及 setX(),這在 Python 中是不被鼓勵的,pythonic 方式是直接存取 attribute ,而當你需要進一步控制時,可以使用 property 達到 getter 以及 setter 效果,如此一來,客戶端的程式不需要修改就能正常運作。

Python 的 threading.Thread 在 2.6 以前只有 getName() 、 setName() 等深受 Java 影響的 methods,後來開始提供 property 的版本了。

慣用

class Egg(object):

    def __init__(self, price):
        self._price = price

    @property
    def price(self):
        return self._price * RATE
    
    @price.setter
    def price(self, value):
        self._price = value

非慣用

class Egg(object):

    def __init__(self, price):
        self._price = price

    def get_price(self):
        return self._price * RATE

    def set_price(self, value):
        self._price = value

要避免對吃資源的 method 使用 property ,因為存取 attribute 給人的感覺是很輕量的操作,這樣使用該物件的人就有可能忽視效能的問題。

Back to Outline


BIFs

BIF 是 Built-in Function 的縮寫,Python 提供許多好用內建函數可以使用,沒事可以多多瀏覽下面的網頁,避免重造輪子。
https://docs.python.org/2/library/functions.html

這裡列出幾個例子

all()

慣用

def is_valid(file_names):
    return all('py' in name for name in file_names)

非慣用

def is_valid(file_names):
    for name in file_names:
        if not 'py' in name:
            return False
    return True

any()

慣用

def is_dangerous(sql):
    actions = ('update', 'delete', 'replace')
    return any(sql.startswith(action) for action in actions)

非慣用

def is_dangerous(sql):
    actions = ('update', 'delete', 'replace')
    for action in actions:
        if sql.startswith(action):
            return True
    return False

sum()

prices = [100, 300, 50, 600]
total = sum(prices)

Back to Outline


Context Managers

context managers 可供 with/as 使用,保證某些動作一定能執行,可取代 try/finally,讓你省掉一些麻煩,許多地方都可以看到它的存在。

經典的例子就是對檔案的操作:

慣用

with open('tmp.txt', 'w') as f:
    f.write('TEST')

非慣用

f = open('tmp.txt', 'w')
try:
    f.write('TEST')
finally:
    f.close()

除了 file objects,threading 裡頭有好幾個物件也提供這樣的操作。假設你操作的物件不是 context manager,那麼記得使用 try/finally 來確保資源被釋放掉。

實現 context managers 有兩種方式,一種是有 enter 以及 exit 的 class ,另一種方式是 generator function 搭配 decorator contextlib.contextmanager。詳情請看 http://pymotw.com/2/contextlib/

Back to Outline


太長怎麼辦

PEP 8 訂出每行最多 79 個字元

Limit all lines to a maximum of 79 characters.

雖然不少人對這有意見,有的是訂出 120 個字元,無論如何可讀性優先。舉個例子, SQL 字串有可能很長一大串,通通塞在同一行會嚴重影響可讀性,這時就須將字串分成多行。

慣用

sql = (
    "SELECT name, product, price "
    "FROM production.product "
    "WHERE description = 'TEST' "
    "AND days < 10 "
    "ORDER BY name DESC"
)

非慣用

sql = "SELECT name, product, price " \
      "FROM production.product " \
      "WHERE description = 'TEST' " \
      "AND days < 10 " \
      "ORDER BY name DESC"

使用括號來達到接續的作用是很常見且較好的作法,因為使用 \ 不僅麻煩,而且很有可能不小心在後面多了空白字元,造成錯誤。

參考: Implicit line joining

Back to Outline