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
,如果有匹配则返回匹配内容。