Python/編程習慣用法

Python 是一種強慣用法的語言:通常只有一種最佳方式來做某事(w:編程習慣用法),而不是多種方式:Perl語言的「不只一種方法來做一件事」不是Python的座右銘。

本節從一些一般原則開始,然後介紹該語言,重點介紹如何慣用標準庫中的操作、數據類型和模塊。

原理

編輯

使用 exceptions進行錯誤檢查,遵循EAFP(請求原諒比請求許可更容易)而不是LBYL(三思而後行):將可能失敗的操作放在try...except塊中。

使用 上下文管理器管理資源,如文件。使用finally進行臨時清理,但最好編寫上下文管理器來封裝它。

使用屬性,而不是getter/setter方法。

使用字典來記錄動態記錄,使用類來記錄靜態記錄(對於簡單類,使用 collections.namedtuple):如果記錄始終具有相同的字段,則在類中明確說明;如果字段可能有所不同(存在或不存在),請使用字典。

使用 _ 表示一次性變量,例如在返回元組時丟棄返回值,或指示忽略參數(例如,當接口需要時)。您可以使用 *_, **__丟棄傳遞給函數的位置參數或關鍵字參數:這些參數對應於通常的*args, **kwargs參數,但被明確丟棄。您還可以在位置參數或命名參數(在您使用的參數之後)之外使用這些參數,這樣您就可以使用一些參數並丟棄多餘的參數。

使用隱式True/False(真/假值),除非需要區分假值,例如 None、0和[],在這種情況下使用顯式檢查,例如 is None== 0

try、for、while 之後使用可選的 else 子句,而不僅僅是 if

導入

編輯

對於可讀且健壯的代碼,僅導入模塊,而不是名稱(如函數或類),因為這會創建一個新的(名稱)綁定,而該綁定不一定與現有綁定同步。[1] 例如,給定一個定義函數 f 的模塊 m,使用 from m import f 導入該函數意味着如果將其中任何一個分配給 m.ff,則它們可以不同(創建新的綁定)。

實際上,這經常被忽略,特別是對於小規模代碼,因為導入後更改模塊的情況很少見,所以這很少成為問題,並且類和函數都是從模塊導入的,因此可以不帶前綴引用它們。但是,對於健壯的大規模代碼,這是一條重要規則,因為它可能會產生非常微妙的錯誤。

對於類型較少的健壯代碼,可以使用重命名導入來縮寫較長的模塊名稱:

import module_with_very_long_name as vl
vl.f()  # easier than module_with_very_long_name.f, but still robust

請注意,使用 from 導入子模塊(或子包)是完全沒問題的:

from p import sm  # completely fine
sm.f()

運算

編輯
交換值
b, a = a, b
在非零值上的屬性訪問

要訪問可能是對象或可能是 None 的值的屬性(尤其是調用方法),請使用 and 的布爾快捷方式:

a and a.x
a and a.f()

對於正則表達式匹配特別有用:

match and match.group(0)
in

使用 in 進行子字符串檢查。

數據類型

編輯

所有的序列類型

編輯
迭代期間索引

如果您需要跟蹤可迭代對象的迭代索引,請使用 enumerate()同時獲得索引和值:

for i, x in enumerate(l):
    # ...

反慣用法:

for i in range(len(l)):
    x = l[i]  # why did you go from list to numbers back to the list?
    # ...
查找第一個匹配元素

Python 序列確實有一個 index 方法,但它會返回序列中特定值第一次出現的索引。要查找滿足條件的值的第一次出現,請使用 next 和生成器表達式:

try:
    x = next(i for i, n in enumerate(l) if n > 0)
except StopIteration:
    print('No positive numbers')
else:
    print('The index of the first positive number is', x)

如果您需要的是值,而不是其出現的索引,您可以直接通過以下方式獲取它:

try:
    x = next(n for n in l if n > 0)
except StopIteration:
    print('No positive numbers')
else:
    print('The first positive number is', x)

這種構造的原因有兩個:

  • 異常讓您發出「未找到匹配項」的信號(它們解決了半謂詞問題):由於您返回的是單個值(而不是索引),因此無法在值中返回該值。
  • 生成器表達式讓您無需 lambda 或引入新語法即可使用表達式。
截斷

對於可變序列,請使用 del,而不是重新分配給切片:

del l[j:]
del l[:i]

反慣用法:

l = l[:j]
l = l[i:]

最簡單的原因是 del 明確表明了您的意圖:您正在截斷。

更微妙的是,切片會創建對同一列表的另一個引用(因為列表是可變的),然後無法訪問的數據可以被垃圾收集,但通常這是稍後完成的。相反,刪除會立即就地修改列表(這比創建切片然後將其分配給現有變量更快),並允許 Python 立即釋放已刪除的元素,而不是等待垃圾收集。

在某些情況下,您「確實」想要同一列表的 2 個切片 - 雖然這在基本編程中很少見,除了在 for 循環中對切片進行一次迭代 - 但您很少會想要對整個列表進行切片,然後用切片替換原始列表變量(但不更改另一個切片!),如以下有趣的代碼所示:

m = l
l = l[i:j]  # why not m = l[i:j] ?
來自可迭代對象的排序列表

您可以直接從任何可迭代對象創建排序列表,而無需先創建列表然後對其進行排序。這些包括集合和字典(按鍵迭代):

s = {1, 'a', ...}
l = sorted(s)
d = {'a': 1, ...}
l = sorted(d)

使用元組表示常量序列。這很少是必要的(主要是在用作字典中的鍵時),但可以使意圖更明確。

字符串

編輯
子字符串

使用 in 進行子字符串檢查。

但是,不要使用 in 檢查字符串是否為單字符匹配,因為它會匹配子字符串並返回虛假匹配 - 而是使用有效值的元組。例如,以下是錯誤的:

def valid_sign(sign):
    return sign in '+-'  # wrong, returns true for sign == '+-'

相反,使用元組:

def valid_sign(sign):
    return sign in ('+', '-')
構建字符串

要逐步生成長字符串,請構建一個列表,然後使用 '' 將其連接起來 - 如果構建的是文本文件,則使用換行符(在這種情況下不要忘記最後的換行符!)。這比附加到字符串更快更清晰,後者通常很「慢」。(原則上,字符串的總長度和添加次數可以是  ,如果各部分大小相似,則為  。)

但是,某些版本的 CPython 中有一些優化,可以使簡單的字符串附加更快 - CPython 2.5+ 中的字符串附加和 CPython 3.0+ 中的字節串附加都很快,但對於構​​建 Unicode 字符串(Python 2 中的 unicode,Python 3 中的字符串),連接更快。如果進行廣泛的字符串操作,請注意這一點並分析您的代碼。有關詳細信息,請參閱 性能提示:字符串連接連接測試代碼

不要這樣做:

s = ''
for x in l:
    # this makes a new string every iteration, because strings are immutable
    s += x

而是要:

# ...
# l.append(x)
s = ''.join(l)

你甚至可以使用非常高效的生成器表達式:

s = ''.join(f(x) for x in l)

如果您確實想要一個可變的字符串類對象,您可以使用 StringIO

字典類型

編輯

要遍歷字典,可以是鍵、值,或者兩者:

# Iterate over keys
for k in d:
    ...

# Iterate over values, Python 3
for v in d.values():
    ...

# Iterate over values, Python 2
# In Python 2, dict.values() returns a copy
for v in d.itervalues():
    ...

# Iterate over keys and values, Python 3
for k, v in d.items():
    ...

# Iterate over values, Python 2
# In Python 2, dict.items() returns a copy
for k, v in d.iteritems():
    ...

反模式:

for k, _ in d.items():  # instead: for k in d:
    ...
for _, v in d.items():  # instead: for v in d.values()
    ...

修復:

  • setdefault
  • 通常最好使用 collections.defaultdict

dict.get 很有用,但使用 dict.get 然後檢查它是否為 None 作為測試鍵是否在字典中的方式是一種反習慣用法,因為 None 是一個潛在值,並且可以直接檢查鍵是否在字典中。但是,如果這不是一個潛在值,則可以使用 get 並與 None 進行比較。

if 'k' in d:
    # ... d['k']

反習語(除非 None 不是潛在值):

v = d.get('k')
if v is not None:
    # ... v
來自鍵和值的並行序列的字典

使用 zip 作為:dict(zip(keys, values))

模塊

編輯

如果找到則匹配,否則

match = re.match(r, s)
return match and match.group(0)

...如果沒有匹配則返回 None,如果有匹配則返回匹配內容。

參考文獻

編輯


進一步閱讀

編輯