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.f
和f
,則它們可以不同(創建新的綁定)。
實際上,這經常被忽略,特別是對於小規模代碼,因為導入後更改模塊的情況很少見,所以這很少成為問題,並且類和函數都是從模塊導入的,因此可以不帶前綴引用它們。但是,對於健壯的大規模代碼,這是一條重要規則,因為它可能會產生非常微妙的錯誤。
對於類型較少的健壯代碼,可以使用重命名導入來縮寫較長的模塊名稱:
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)
Tuple
編輯使用元組表示常量序列。這很少是必要的(主要是在用作字典中的鍵時),但可以使意圖更明確。
字符串
編輯- 子字符串
使用 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
。
- Efficient String Concatenation in Python – old article (so benchmarks out of date), but gives overview of some techniques.
字典類型
編輯要遍歷字典,可以是鍵、值,或者兩者:
# 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))
模塊
編輯re
編輯如果找到則匹配,否則 无
:
match = re.match(r, s)
return match and match.group(0)
...如果沒有匹配則返回 None
,如果有匹配則返回匹配內容。