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.ff 进行赋值,它们的值会不一致,因为它们分别创建了新的绑定。

在实际开发中,这通常被忽略,特别是在小规模代码中,因为修改导入后模块的内容很少见,所以这通常不是问题,模块中的函数被导入后可以直接使用,无需前缀。但是,在健壮的大规模代码中,这条规则很重要,因为它可能会引发非常细微的错误

对于健壮的代码且减少键入,可以使用重命名导入来简化长模块名:

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)
      

      这样做的原因有两个:

      1. 异常允许你标示“没有找到匹配项”(解决了半谓词问题):因为你返回的是一个值(而不是索引),所以无法将其作为值返回。
      2. 生成器表达式让你使用一个表达式而不需要引入 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,如果有匹配,则返回匹配的内容。

Last modified: Friday, 31 January 2025, 12:52 AM