Python 编程/习语
Python 是一种强习语的语言:通常有一种最佳方法来完成某件事(一种编程习语),而不是很多方法:“不止一种方法可以做到”不是 Python 的座右铭。
本节从一些一般原则开始,然后遍历语言,重点介绍如何在标准库中习惯性地使用操作、数据类型和模块。
使用异常进行错误检查,遵循 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)
使用元组表示常量序列。这很少必要(主要是在用作字典中的键时),但这会使意图明确。
- 子字符串
使用 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 中的 string),连接速度更快。如果进行大量的字符串操作,请注意这一点并分析你的代码。有关详细信息,请参阅性能提示:字符串连接和连接测试代码。
不要这样做
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
。
- Python 中的高效字符串连接——旧文章(因此基准测试已过时),但提供了对一些技术的概述。
迭代字典时,可以遍历键、值或两者
# 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()
...
FIXME
- setdefault
- 通常最好使用 collections.defaultdict
dict.get
很实用,但使用 dict.get
然后检查它是否为 None
来测试键是否在字典中是一种反模式,因为 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))
如果找到则匹配,否则为 None
match = re.match(r, s)
return match and match.group(0)
... 如果没有匹配,则返回 None
,如果有匹配,则返回匹配内容。
- “Python 中的习语和反模式”, Moshe Zadka
- “PEP 20 -- Python 之禅”, Tim Peters