跳转到内容

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.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)

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

字典类型

[编辑]

要遍历字典,可以是键、值,或者两者:

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

参考文献

[编辑]


进一步阅读

[编辑]