Python编程
Python 是一种强烈惯用语的语言:通常做某件事有一个最佳的方式(编程惯用法),而不是多种方式:“There’s more than one way to do it” 并不是 Python 的座右铭。
本节开始介绍一些基本原则,然后逐步讲解语言中的惯用法,重点介绍如何惯用地使用操作、数据类型和标准库中的模块。
原则
- 使用异常进行错误检查,遵循 EAFP(It's Easier to Ask Forgiveness than Permission),而不是 LBYL(Look Before You Leap):将可能失败的操作放入 try...except 块中。
- 使用上下文管理器来管理资源,如文件。使用 finally 进行临时清理,但建议编写上下文管理器来封装这些操作。
- 使用属性而非 getter/setter 方法。
- 使用字典表示动态记录,使用类表示静态记录(对于简单的类,使用 collections.namedtuple):如果记录总是有相同的字段,则在类中显式定义这些字段;如果字段可能会变化(存在或不存在),则使用字典。
- 使用 _ 表示一次性变量,如丢弃返回值时使用元组返回,或表示参数被忽略(在需要接口时)。你也可以使用 *_, **__ 来丢弃传递给函数的位置参数或关键字参数:这些对应于通常的 *args, **kwargs 参数,但被显式丢弃。你也可以在使用的位置参数或命名参数之后使用它们,允许你使用一些参数并丢弃多余的参数。
- 使用隐式的 True/False(真值/假值),除非需要区分假值,如 None、0 和 [],此时应使用显式检查,如
is None
或== 0
。 - 使用可选的 else 子句,在 try、for 和 while 后都可以使用,而不仅仅是 if。
导入
为了提高代码的可读性和健壮性,只导入模块,而不是名称(如函数或类),因为这样会创建一个新的(名称)绑定,而这个绑定不一定与现有绑定同步。例如,给定一个定义了函数 f 的模块 m,通过 from m import f
导入该函数时,如果对 m.f
或 f
进行赋值,它们的值会不一致,因为它们分别创建了新的绑定。
在实际开发中,这通常被忽略,特别是在小规模代码中,因为修改导入后模块的内容很少见,所以这通常不是问题,模块中的类和函数被导入后可以直接使用,无需前缀。但是,在健壮的大规模代码中,这条规则很重要,因为它可能会引发非常细微的错误。
对于健壮的代码且减少键入,可以使用重命名导入来简化长模块名:
import module_with_very_long_name as vl
vl.f() # 比module_with_very_long_name.f 更简洁,但依然健壮
请注意,从包中导入子模块(或子包)使用 from
是完全可以的:
from p import sm # 完全可以
sm.f()
操作
-
交换值
b, a = a, b
-
对可能为 null 的值进行属性访问
要访问一个可能是对象,也可能是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] # 为什么从列表转到数字,再转回列表? # ...
-
找到第一个匹配的元素
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
可以清晰地表达你的意图:你正在截断数据。更微妙的是,切片会创建对同一个列表的另一个引用(因为列表是可变的),然后无法访问的数据可以被垃圾回收,但通常这是稍后才发生的。使用
del
则会立即就地修改列表(这比创建一个切片然后将其分配给现有变量要快),并允许 Python 立即释放已删除的元素,而不是等待垃圾回收。在某些情况下,你确实需要对同一个列表进行两个切片——虽然在基本编程中这种情况很少见,除非是在
for
循环中迭代切片,但通常你不会想先切出整个列表的一个切片,然后替换原来的列表变量为切片(但不更改其他切片!),例如下面这个看起来有点奇怪的代码:m = l l = l[i:j] # 为什么不直接是 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 '+-' # 错误,会在 sign == '+-' 时返回 True
正确的做法是使用元组:
def valid_sign(sign): return sign in ('+', '-')
-
构建字符串
为了逐步构建一个长字符串,应该先构建一个列表,然后用
''
(或换行符,如果你在构建文本文件的话)将它们连接起来。这样比在字符串上反复追加要快而且清晰。字符串追加通常是慢的,原则上可能会是 O(nk) 复杂度,其中n
是字符串的总长度,k
是追加的次数(如果每次追加的字符串大小相似,那么时间复杂度可能是 O(n²))。然而,在某些版本的 CPython 中,简单的字符串追加已经变得很快——在 CPython 2.5+ 和 CPython 3.0+ 中,字节串的追加非常快,但对于构建 Unicode 字符串(Python 2 中的 unicode,Python 3 中的 string),使用
join
更快。如果进行大量字符串操作,请留意这一点并对代码进行性能分析。不要这样做:
s = '' for x in l: # 每次迭代都会创建一个新字符串,因为字符串是不可变的 s += x
改成这样:
s = ''.join(l)
你甚至可以使用生成器表达式,这非常高效:
s = ''.join(f(x) for x in l)
如果你需要一个可变的类字符串对象,你可以使用
StringIO
。
字典
-
迭代字典
可以通过字典的键、值或两者来迭代:
-
迭代键:
for k in d: ...
-
迭代值(Python 3):
for v in d.values(): ...
-
迭代值(Python 2):
for v in d.itervalues(): ...
-
迭代键和值(Python 3):
for k, v in d.items(): ...
-
迭代键和值(Python 2):
for k, v in d.iteritems(): ...
反模式:
- 如果只需要键,避免使用
for k, _ in d.items()
,改为for k in d:
。 - 如果只需要值,避免使用
for _, v in d.items()
,改为for v in d.values()
。
FIXME:
- 使用
setdefault
时,通常最好使用collections.defaultdict
。 dict.get
是有用的,但如果将其与None
进行比较作为测试键是否存在的方法,这是反惯用法,因为None
是一个潜在的值,检查键是否存在可以直接使用in
来做。
简单做法:
if 'k' in d: # ... d['k']
反惯用法(除非 None 不是潜在值):
v = d.get('k') if v is not None: # ... v
-
-
从并行的键和值序列创建字典
使用
zip
创建字典:dict(zip(keys, values))
模块
-
re
如果匹配成功,则返回匹配对象,否则返回
None
:match = re.match(r, s) return match and match.group(0)
如果没有匹配,返回
None
,如果有匹配,则返回匹配的内容。
-
-