跳转至内容

Python 编程/习语

来自 Wikibooks,开放世界中的开放书籍

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.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 中的 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

迭代字典时,可以遍历键、值或两者

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

参考文献

[编辑 | 编辑源代码]

进一步阅读

[编辑 | 编辑源代码]
华夏公益教科书