styleguide

Google Python 代码风格指南

目录

1 背景

Python 是 Google 使用的主要动态语言。本代码风格指南列出了 Python 程序的应该做和不应该做

为了帮助您正确格式化代码,我们创建了一个 Vim 的设置文件。对于 Emacs,默认设置应该没问题。

许多团队使用 BlackPyink 自动格式化程序,以避免在格式方面争论。

2 Python 语言规则

2.1 Lint

使用此 pylintrc 在您的代码上运行 pylint

2.1.1 定义

pylint 是一个用于查找 Python 源代码中的错误和风格问题的工具。 它可以找到那些通常被 C 和 C++ 等动态性较差的语言的编译器捕获的问题。 由于 Python 的动态特性,某些警告可能不正确;但是,错误的警告应该很少见。

2.1.2 优点

捕获容易忽略的错误,例如拼写错误、在使用变量之前使用等。

2.1.3 缺点

pylint 并不完美。 为了利用它,有时我们需要绕过它,抑制其警告或修复它。

2.1.4 决定

确保在您的代码上运行 pylint

如果警告不合适,请抑制警告,以便其他问题不会被隐藏。 要抑制警告,您可以设置行级注释

def do_PUT(self):  # WSGI name, so pylint: disable=invalid-name
  ...

pylint 警告都由符号名称标识(empty-docstring)。Google 特定的警告以 g- 开头。

如果抑制的原因从符号名称中不清楚,请添加解释。

以这种方式抑制的优点是我们可以轻松搜索抑制并重新访问它们。

您可以通过执行以下操作获取 pylint 警告的列表

pylint --list-msgs

要获取有关特定消息的更多信息,请使用

pylint --help-msg=invalid-name

首选 pylint: disable,而不是已弃用的旧形式 pylint: disable-msg

可以通过删除函数开头的变量来抑制未使用的参数警告。 始终包含注释以解释为什么要删除它。“未使用的。”就足够了。 例如

def viking_cafe_order(spam: str, beans: str, eggs: str | None = None) -> str:
    del beans, eggs  # Unused by vikings.
    return spam + spam + spam

抑制此警告的其他常见形式包括使用 “_” 作为未使用参数的标识符,或者在参数名称前加上 “unused_”,或者将它们分配给 “_”。 允许这些形式,但不再鼓励。 这些会破坏按名称传递参数的调用者,并且不会强制要求实际未使用参数。

2.2 导入

仅对包和模块使用 import 语句,不对单个类型、类或函数使用。

2.2.1 定义

用于从一个模块向另一个模块共享代码的可重用机制。

2.2.2 优点

命名空间管理约定很简单。 每个标识符的来源都以一致的方式指示; x.Obj 表示对象 Obj 在模块 x 中定义。

2.2.3 缺点

模块名称仍然可能冲突。 一些模块名称不方便地长。

2.2.4 决定

例如,模块 sound.effects.echo 可以按如下方式导入

from sound.effects import echo
...
echo.EchoFilter(input, output, delay=0.7, atten=4)

不要在导入中使用相对名称。 即使该模块位于同一包中,也请使用完整的包名称。 这有助于防止意外地两次导入一个包。

2.2.4.1 例外

此规则的例外

2.3 包

使用模块的完整路径名位置导入每个模块。

2.3.1 优点

避免模块名称冲突或由于模块搜索路径不是作者预期的而导致的错误导入。 使查找模块更容易。

2.3.2 缺点

使部署代码更困难,因为您必须复制包层次结构。 对于现代部署机制来说,这并不是真正的问题。

2.3.3 决定

所有新代码都应按其完整的包名称导入每个模块。

导入应如下所示

Yes:
  # Reference absl.flags in code with the complete name (verbose).
  import absl.flags
  from doctor.who import jodie

  _FOO = absl.flags.DEFINE_string(...)
Yes:
  # Reference flags in code with just the module name (common).
  from absl import flags
  from doctor.who import jodie

  _FOO = flags.DEFINE_string(...)

(假设此文件位于 doctor/who/ 中,其中也存在 jodie.py

No:
  # Unclear what module the author wanted and what will be imported.  The actual
  # import behavior depends on external factors controlling sys.path.
  # Which possible jodie module did the author intend to import?
  import jodie

不应假定主二进制文件所在的目录位于 sys.path 中,尽管这发生在某些环境中。 在这种情况下,代码应假定 import jodie 指的是名为 jodie 的第三方或顶级包,而不是本地 jodie.py

2.4 异常

允许异常,但必须小心使用。

2.4.1 定义

异常是一种打破正常控制流以处理错误或其他异常情况的方法。

2.4.2 优点

正常操作代码的控制流不会被错误处理代码搞乱。 它还允许控制流在发生特定情况时跳过多个帧,例如,从 N 个嵌套函数一步返回,而不是必须通过管道传输错误代码。

2.4.3 缺点

可能导致控制流混乱。 在进行库调用时容易错过错误情况。

2.4.4 决定

异常必须遵循某些条件

2.5 可变全局状态

避免可变全局状态。

2.5.1 定义

可以在程序执行期间发生改变的模块级值或类属性。

2.5.2 优点

偶尔有用。

2.5.3 缺点

2.5.4 决定

避免可变全局状态。

在那些极少数需要使用全局状态的情况下,可变全局实体应在模块级别或作为类属性声明,并通过在名称前添加 _ 来使其成为内部实体。如有必要,对可变全局状态的外部访问必须通过公共函数或类方法完成。请参阅下面的命名。请在注释或从注释链接到的文档中解释使用可变全局状态的设计原因。

允许并鼓励使用模块级常量。例如:_MAX_HOLY_HANDGRENADE_COUNT = 3 用于内部使用常量,或者 SIR_LANCELOTS_FAVORITE_COLOR = "blue" 用于公共 API 常量。常量必须使用全部大写字母和下划线命名。请参阅下面的命名

2.6 嵌套/本地/内部类和函数

嵌套的局部函数或类在使用闭包访问局部变量时是可以接受的。内部类是可以接受的。

2.6.1 定义

可以在方法、函数或类内部定义类。可以在方法或函数内部定义函数。嵌套函数具有对封闭范围内定义的变量的只读访问权限。

2.6.2 优点

允许定义仅在非常有限的范围内使用的实用程序类和函数。非常 ADT-y。通常用于实现装饰器。

2.6.3 缺点

嵌套函数和类无法直接测试。嵌套会使外部函数更长且可读性更差。

2.6.4 决定

它们在某些情况下是可以接受的。除非使用闭包访问除 selfcls 之外的局部值,否则避免嵌套函数或类。不要为了对模块用户隐藏函数而嵌套函数。相反,在模块级别为其名称添加 _ 前缀,以便测试仍然可以访问它。

2.7 推导式 & 生成器表达式

简单情况下可以使用。

2.7.1 定义

列表、字典和集合推导以及生成器表达式提供了一种简洁高效的方式来创建容器类型和迭代器,而无需使用传统的循环、map()filter()lambda

2.7.2 优点

简单的推导式可以比其他字典、列表或集合创建技术更清晰和更简单。生成器表达式可能非常高效,因为它们避免了完全创建列表。

2.7.3 缺点

复杂的推导式或生成器表达式可能难以阅读。

2.7.4 决定

允许使用推导式,但允许多个 for 子句或过滤器表达式。为了可读性而优化,而不是简洁性。

Yes:
  result = [mapping_expr for value in iterable if filter_expr]

  result = [
      is_valid(metric={'key': value})
      for value in interesting_iterable
      if a_longer_filter_expression(value)
  ]

  descriptive_name = [
      transform({'key': key, 'value': value}, color='black')
      for key, value in generate_iterable(some_input)
      if complicated_condition_is_met(key, value)
  ]

  result = []
  for x in range(10):
    for y in range(5):
      if x * y > 10:
        result.append((x, y))

  return {
      x: complicated_transform(x)
      for x in long_generator_function(parameter)
      if x is not None
  }

  return (x**2 for x in range(10))

  unique_names = {user.name for user in users if user is not None}
No:
  result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]

  return (
      (x, y, z)
      for x in range(5)
      for y in range(5)
      if x != y
      for z in range(5)
      if y != z
  )

2.8 默认迭代器和操作符

对于支持默认迭代器和运算符的类型(如列表、字典和文件),请使用默认的迭代器和运算符。

2.8.1 定义

容器类型(如字典和列表)定义了默认的迭代器和成员资格测试运算符(“in”和“not in”)。

2.8.2 优点

默认的迭代器和运算符简单高效。它们直接表达操作,无需额外的函数调用。使用默认运算符的函数是通用的。它可以与任何支持该操作的类型一起使用。

2.8.3 缺点

您无法通过读取方法名称来判断对象的类型(除非变量具有类型注释)。这也是一个优点。

2.8.4 决定

对于支持默认迭代器和运算符的类型(如列表、字典和文件),请使用默认的迭代器和运算符。内置类型也定义了迭代器方法。首选这些方法而不是返回列表的方法,但迭代容器时不要修改它。

Yes:  for key in adict: ...
      if obj in alist: ...
      for line in afile: ...
      for k, v in adict.items(): ...
No:   for key in adict.keys(): ...
      for line in afile.readlines(): ...

2.9 生成器

根据需要使用生成器。

2.9.1 定义

生成器函数返回一个迭代器,该迭代器在每次执行 yield 语句时都会生成一个值。在生成一个值后,生成器函数的运行时状态将暂停,直到需要下一个值为止。

2.9.2 优点

代码更简单,因为局部变量和控制流的状态为每次调用保留。生成器比一次创建整个值列表的函数使用的内存更少。

2.9.3 缺点

在生成器被耗尽或本身被垃圾回收之前,生成器中的局部变量不会被垃圾回收。

2.9.4 决定

可以。在生成器函数的文档字符串中使用“Yields:”而不是“Returns:”。

如果生成器管理着昂贵的资源,请确保强制清理。

清理的一个好方法是使用上下文管理器 PEP-0533 包装生成器。

2.10 Lambda 函数

单行代码可以使用。与使用 lambdamap()filter() 相比,更喜欢生成器表达式。

2.10.1 定义

Lambdas 在表达式中定义匿名函数,而不是语句。

2.10.2 优点

方便。

2.10.3 缺点

比局部函数更难阅读和调试。缺少名称意味着堆栈跟踪更难以理解。表达能力有限,因为该函数只能包含一个表达式。

2.10.4 决定

允许使用 Lambdas。如果 lambda 函数中的代码跨越多行或长度超过 60-80 个字符,则最好将其定义为常规的嵌套函数

对于乘法等常见操作,请使用 operator 模块中的函数,而不是 lambda 函数。例如,首选 operator.mul 而不是 lambda x, y: x * y

2.11 条件表达式

简单情况下可以使用。

2.11.1 定义

条件表达式(有时称为“三元运算符”)是一种为 if 语句提供更短语法的机制。例如:x = 1 if cond else 2

2.11.2 优点

比 if 语句更短更方便。

2.11.3 缺点

可能比 if 语句更难阅读。如果表达式很长,则可能难以找到条件。

2.11.4 决定

简单情况下可以使用。每个部分必须在一行上:true-expression, if-expression, else-expression。当事情变得更复杂时,请使用完整的 if 语句。

Yes:
    one_line = 'yes' if predicate(value) else 'no'
    slightly_split = ('yes' if predicate(value)
                      else 'no, nein, nyet')
    the_longest_ternary_style_that_can_be_done = (
        'yes, true, affirmative, confirmed, correct'
        if predicate(value)
        else 'no, false, negative, nay')
No:
    bad_line_breaking = ('yes' if predicate(value) else
                         'no')
    portion_too_long = ('yes'
                        if some_long_module.some_long_predicate_function(
                            really_long_variable_name)
                        else 'no, false, negative, nay')

2.12 默认参数值

大多数情况下可以使用。

2.12.1 定义

您可以在函数的参数列表末尾为变量指定值,例如,def foo(a, b=0):。如果只使用一个参数调用 foo,则 b 设置为 0。如果使用两个参数调用它,则 b 具有第二个参数的值。

2.12.2 优点

通常,您有一个使用大量默认值的函数,但在极少数情况下,您想覆盖默认值。默认参数值提供了一种简单的方法来实现此目的,而无需为罕见的异常定义大量函数。由于 Python 不支持重载方法/函数,因此默认参数是“伪造”重载行为的一种简单方法。

2.12.3 缺点

默认参数在模块加载时评估一次。如果参数是可变对象(例如列表或字典),这可能会导致问题。如果函数修改了对象(例如,通过将项目附加到列表),则默认值会被修改。

2.12.4 决定

可以使用,但有以下注意事项

不要在函数或方法定义中使用可变对象作为默认值。

Yes: def foo(a, b=None):
         if b is None:
             b = []
Yes: def foo(a, b: Sequence | None = None):
         if b is None:
             b = []
Yes: def foo(a, b: Sequence = ()):  # Empty tuple OK since tuples are immutable.
         ...
from absl import flags
_FOO = flags.DEFINE_string(...)

No:  def foo(a, b=[]):
         ...
No:  def foo(a, b=time.time()):  # Is `b` supposed to represent when this module was loaded?
         ...
No:  def foo(a, b=_FOO.value):  # sys.argv has not yet been parsed...
         ...
No:  def foo(a, b: Mapping = {}):  # Could still get passed to unchecked code.
         ...

2.13 属性

属性可用于控制需要琐碎计算或逻辑的属性的获取或设置。属性实现必须符合常规属性访问的一般预期:它们便宜、直接且不会令人意外。

2.13.1 定义

一种将获取和设置属性的方法调用包装为标准属性访问的方式。

2.13.2 优点

2.13.3 缺点

2.13.4 决定

允许使用属性,但与运算符重载一样,仅应在必要时使用,并符合典型属性访问的预期;否则请遵循 getter 和 setter 规则。

例如,不允许使用属性来简单地获取和设置内部属性:没有发生计算,因此属性是不必要的(而是使该属性公开)。相比之下,允许使用属性来控制属性访问或计算微不足道的派生值:逻辑简单且不令人意外。

应使用 @property 装饰器 创建属性。手动实现属性描述符被认为是 高级功能

带有属性的继承可能不明显。不要使用属性来实现子类可能想要覆盖和扩展的计算。

2.14 真/假判断

尽可能使用“隐式”假(但有一些注意事项)。

2.14.1 定义

当在布尔上下文中时,Python 将某些值评估为 False。一个快速的“经验法则”是,所有“空”值都被认为是假的,因此 0, None, [], {}, '' 在布尔上下文中都被评估为假。

2.14.2 优点

使用 Python 布尔值的条件更容易阅读且不易出错。在大多数情况下,它们也更快。

2.14.3 缺点

对于 C/C++ 开发人员来说,可能看起来很奇怪。

2.14.4 决定

如果可能,请使用“隐式”假,例如,if foo: 而不是 if foo != []:。但是,您应该记住一些注意事项

2.16 词法作用域

可以使用。

2.16.1 定义

嵌套的 Python 函数可以引用在封闭函数中定义的变量,但不能对其进行赋值。变量绑定使用词法作用域来解析,即基于静态程序文本。在块中对名称的任何赋值都将导致 Python 将对该名称的所有引用视为局部变量,即使该使用在赋值之前。如果出现全局声明,则该名称被视为全局变量。

此功能的使用示例如下

def get_adder(summand1: float) -> Callable[[float], float]:
    """Returns a function that adds numbers to a given number."""
    def adder(summand2: float) -> float:
        return summand1 + summand2

    return adder

2.16.2 优点

通常会产生更清晰、更优雅的代码。对于经验丰富的 Lisp 和 Scheme(以及 Haskell 和 ML 和 …)程序员来说,尤其令人感到舒适。

2.16.3 缺点

可能导致令人困惑的错误,例如基于 PEP-0227 的这个示例

i = 4
def foo(x: Iterable[int]):
    def bar():
        print(i, end='')
    # ...
    # A bunch of code here
    # ...
    for i in x:  # Ah, i *is* local to foo, so this is what bar sees
        print(i, end='')
    bar()

所以 foo([1, 2, 3]) 将打印 1 2 3 3,而不是 1 2 3 4

2.16.4 决定

可以使用。

2.17 函数和方法装饰器

当有明显的优势时,请谨慎使用装饰器。避免使用 staticmethod 并限制使用 classmethod

2.17.1 定义

函数和方法的装饰器(又名“@ 符号”)。一个常见的装饰器是 @property,用于将普通方法转换为动态计算的属性。但是,装饰器语法也允许用户定义的装饰器。具体来说,对于某些函数 my_decorator,以下

class C:
    @my_decorator
    def method(self):
        # method body ...

等价于

class C:
    def method(self):
        # method body ...
    method = my_decorator(method)

2.17.2 优点

优雅地指定对方法的某种转换;该转换可能会消除一些重复代码、强制执行不变量等。

2.17.3 缺点

装饰器可以对函数的参数或返回值执行任意操作,从而导致令人惊讶的隐式行为。此外,装饰器在对象定义时执行。对于模块级对象(类、模块函数等),这发生在导入时。装饰器代码中的故障几乎不可能恢复。

2.17.4 决定

当有明显的优势时,请谨慎使用装饰器。装饰器应遵循与函数相同的导入和命名准则。装饰器文档字符串应清楚地说明该函数是装饰器。为装饰器编写单元测试。

避免装饰器本身的外部依赖项(例如,不要依赖文件、套接字、数据库连接等),因为当装饰器运行时(在导入时,可能来自 pydoc 或其他工具),它们可能不可用。应该(尽可能地)保证使用有效参数调用的装饰器在所有情况下都能成功。

装饰器是“顶级代码”的一个特例 - 有关更多讨论,请参阅 main

除非为了与现有库中定义的 API 集成而被迫使用,否则永远不要使用 staticmethod。改为编写一个模块级函数。

仅在编写命名构造函数或修改必要的全局状态(例如进程范围的缓存)的特定于类的例程时才使用 classmethod

2.18 线程

不要依赖内置类型的原子性。

虽然 Python 的内置数据类型(如字典)似乎具有原子操作,但在某些极端情况下它们不是原子的(例如,如果 __hash____eq__ 被实现为 Python 方法),并且不应依赖于它们的原子性。您也不应依赖原子变量赋值(因为这反过来取决于字典)。

使用 queue 模块的 Queue 数据类型作为线程之间传递数据的首选方式。否则,请使用 threading 模块及其锁定原语。首选条件变量和 threading.Condition 而不是使用较低级别的锁。

2.19 高级特性

避免使用这些特性。

2.19.1 定义

Python 是一种非常灵活的语言,它为您提供了许多高级特性,例如自定义元类、访问字节码、即时编译、动态继承、对象重新父化、导入 hacks、反射(例如 getattr() 的某些用法)、修改系统内部结构、实现自定义清理的 __del__ 方法等。

2.19.2 优点

这些是强大的语言特性。它们可以使您的代码更紧凑。

2.19.3 缺点

当这些“酷”特性不是绝对必要时,使用它们非常诱人。使用底层的不寻常特性的代码更难阅读、理解和调试。起初(对于原始作者来说)并非如此,但当重新访问代码时,它往往比更长但更直观的代码更困难。

2.19.4 决定

避免在您的代码中使用这些特性。

可以使用内部使用这些特性的标准库模块和类(例如,abc.ABCMetadataclassesenum)。

2.20 现代 Python: from __future__ imports

新的语言版本语义更改可能会通过特殊的 future 导入来控制,以便在早期运行时在每个文件的基础上启用它们。

2.20.1 定义

能够通过 from __future__ import 语句启用一些更现代的特性,允许提前使用预期未来 Python 版本中的特性。

2.20.2 优点

事实证明,这可以使运行时版本升级更加顺畅,因为可以在每个文件的基础上进行更改,同时声明兼容性并防止这些文件中的回归。现代代码更易于维护,因为它不太可能累积技术债务,这将在未来的运行时升级期间出现问题。

2.20.3 缺点

此类代码可能无法在引入所需 future 语句之前的非常旧的解释器版本上运行。在支持极其广泛的环境的项目中,对这种需求更为常见。

2.20.4 决定

from __future__ imports

鼓励使用 from __future__ import 语句。它允许给定的源文件从今天开始使用更现代的 Python 语法特性。一旦您不再需要在特性隐藏在 __future__ 导入后面的版本上运行,请随意删除这些行。

在可能在 3.5 这样的旧版本而不是 >= 3.7 版本上执行的代码中,导入

from __future__ import generator_stop

有关更多信息,请阅读 Python future 语句定义 文档。

请不要删除这些导入,除非您确信该代码仅在足够现代的环境中使用。即使您今天没有在代码中使用特定 future 导入启用的特性,将其保留在文件中也可以防止以后对代码的修改意外地依赖于旧的行为。

根据您的需要使用其他 from __future__ 导入语句。

2.21 类型注解代码

您可以使用 类型提示注释 Python 代码。在构建时使用类型检查工具(如 pytype)检查代码。在大多数情况下,如果可行,类型注释位于源文件中。对于第三方或扩展模块,注释可以位于 存根 .pyi 文件中。

2.21.1 定义

类型注释(或“类型提示”)用于函数或方法参数和返回值

def func(a: int) -> list[int]:

您还可以使用类似的语法声明变量的类型

a: SomeType = some_func()

2.21.2 优点

类型注释提高了代码的可读性和可维护性。类型检查器会将许多运行时错误转换为构建时错误,并降低您使用 高级特性的能力。

2.21.3 缺点

您必须保持类型声明的最新状态。您可能会看到您认为有效的代码的类型错误。使用 类型检查器可能会降低您使用 高级特性的能力。

2.21.4 决定

强烈建议您在更新代码时启用 Python 类型分析。在添加或修改公共 API 时,请包含类型注释,并在构建系统中启用通过 pytype 进行的检查。由于静态分析对于 Python 来说相对较新,我们承认不希望的副作用(例如错误推断的类型)可能会阻止某些项目采用。在这些情况下,鼓励作者添加带有 TODO 的注释或链接到错误,描述当前阻止在 BUILD 文件或代码本身中采用类型注释的问题(如果合适)。

3 Python 风格规则

3.1 分号

不要用分号结束您的行,也不要使用分号将两个语句放在同一行上。

3.2 行长度

最大行长度为 *80 个字符*。

对 80 个字符限制的显式例外

不要使用反斜杠进行 显式行继续

相反,利用 Python 的 括号、方括号和大括号内的隐式行连接。如有必要,您可以在表达式周围添加一对额外的括号。

请注意,此规则不禁止字符串中的反斜杠转义换行符(请参阅下方)。

Yes: foo_bar(self, width, height, color='black', design=None, x='foo',
             emphasis=None, highlight=0)

Yes: if (width == 0 and height == 0 and
         color == 'red' and emphasis == 'strong'):

     (bridge_questions.clarification_on
      .average_airspeed_of.unladen_swallow) = 'African or European?'

     with (
         very_long_first_expression_function() as spam,
         very_long_second_expression_function() as beans,
         third_thing() as eggs,
     ):
       place_order(eggs, beans, spam, beans)

No:  if width == 0 and height == 0 and \
         color == 'red' and emphasis == 'strong':

     bridge_questions.clarification_on \
         .average_airspeed_of.unladen_swallow = 'African or European?'

     with very_long_first_expression_function() as spam, \
           very_long_second_expression_function() as beans, \
           third_thing() as eggs:
       place_order(eggs, beans, spam, beans)

当文字字符串无法放在一行上时,请使用括号进行隐式行连接。

x = ('This will build a very long long '
     'long long long long long long string')

最好在尽可能高的语法级别上断行。如果必须断两次行,则两次都在相同的语法级别上断开。

Yes: bridgekeeper.answer(
         name="Arthur", quest=questlib.find(owner="Arthur", perilous=True))

     answer = (a_long_line().of_chained_methods()
               .that_eventually_provides().an_answer())

     if (
         config is None
         or 'editor.language' not in config
         or config['editor.language'].use_spaces is False
     ):
       use_tabs()
No: bridgekeeper.answer(name="Arthur", quest=questlib.find(
        owner="Arthur", perilous=True))

    answer = a_long_line().of_chained_methods().that_eventually_provides(
        ).an_answer()

    if (config is None or 'editor.language' not in config or config[
        'editor.language'].use_spaces is False):
      use_tabs()

在注释中,如有必要,将长 URL 放在单独的行上。

Yes:  # See details at
      # http://www.example.com/us/developer/documentation/api/content/v2.0/csv_file_name_extension_full_specification.html
No:  # See details at
     # http://www.example.com/us/developer/documentation/api/content/\
     # v2.0/csv_file_name_extension_full_specification.html

请注意上面行继续示例中元素的缩进;有关说明,请参阅缩进部分。

文档字符串摘要行必须保持在 80 个字符限制内。

在所有其他情况下,如果一行代码超过 80 个字符,并且 BlackPyink 自动格式化工具无法将其缩减到限制以下,则允许该行超过此最大长度。 建议作者在合理的情况下,根据上述说明手动将行拆分。

3.3 圆括号

谨慎使用括号。

使用括号包裹元组是可以的(但不是必需的)。除非使用括号进行隐式行延续或指示元组,否则不要在 return 语句或条件语句中使用它们。

Yes: if foo:
         bar()
     while x:
         x = bar()
     if x and y:
         bar()
     if not x:
         bar()
     # For a 1 item tuple the ()s are more visually obvious than the comma.
     onesie = (foo,)
     return foo
     return spam, beans
     return (spam, beans)
     for (x, y) in dict.items(): ...
No:  if (x):
         bar()
     if not(x):
         bar()
     return (foo)

3.4 缩进

使用4 个空格缩进你的代码块。

永远不要使用制表符。 隐式行延续应该垂直对齐包裹的元素(参见行长度示例),或者使用悬挂 4 个空格的缩进。 闭合(圆括号、方括号或花括号)可以放在表达式的末尾,也可以放在单独的行上,但随后应与相应的开始括号所在的行缩进相同。

Yes:   # Aligned with opening delimiter.
       foo = long_function_name(var_one, var_two,
                                var_three, var_four)
       meal = (spam,
               beans)

       # Aligned with opening delimiter in a dictionary.
       foo = {
           'long_dictionary_key': value1 +
                                  value2,
           ...
       }

       # 4-space hanging indent; nothing on first line.
       foo = long_function_name(
           var_one, var_two, var_three,
           var_four)
       meal = (
           spam,
           beans)

       # 4-space hanging indent; nothing on first line,
       # closing parenthesis on a new line.
       foo = long_function_name(
           var_one, var_two, var_three,
           var_four
       )
       meal = (
           spam,
           beans,
       )

       # 4-space hanging indent in a dictionary.
       foo = {
           'long_dictionary_key':
               long_dictionary_value,
           ...
       }
No:    # Stuff on first line forbidden.
       foo = long_function_name(var_one, var_two,
           var_three, var_four)
       meal = (spam,
           beans)

       # 2-space hanging indent forbidden.
       foo = long_function_name(
         var_one, var_two, var_three,
         var_four)

       # No hanging indent in a dictionary.
       foo = {
           'long_dictionary_key':
           long_dictionary_value,
           ...
       }

3.4.1 项目序列中允许末尾逗号吗?

仅当闭合容器标记 ])} 与最后一个元素不在同一行时,以及对于包含单个元素的元组,才建议使用项目序列中的尾随逗号。 尾随逗号的存在也用作提示,指示我们的 Python 代码自动格式化工具 BlackPyink 在最终元素后存在 , 时,自动将项目容器格式化为每行一个项目。

Yes:   golomb3 = [0, 1, 3]
       golomb4 = [
           0,
           1,
           4,
           6,
       ]
No:    golomb4 = [
           0,
           1,
           4,
           6,]

3.5 空行

顶级定义(无论是函数定义还是类定义)之间使用两个空行。 方法定义之间以及 class 的文档字符串和第一个方法之间使用一个空行。 在 def 行之后没有空行。 在函数或方法中,根据你的判断使用单个空行。

空行不需要锚定到定义。 例如,紧接在函数、类和方法定义之前的相关注释可能是有意义的。 考虑一下你的注释是否作为文档字符串的一部分更有用。

3.6 空格

遵循标准排版规则,在标点符号周围使用空格。

括号、方括号或花括号内没有空格。

Yes: spam(ham[1], {'eggs': 2}, [])
No:  spam( ham[ 1 ], { 'eggs': 2 }, [ ] )

逗号、分号或冒号前没有空格。 在逗号、分号或冒号后使用空格,除非在行尾。

Yes: if x == 4:
         print(x, y)
     x, y = y, x
No:  if x == 4 :
         print(x , y)
     x , y = y , x

开始参数列表、索引或切片的左括号/方括号前没有空格。

Yes: spam(1)
No:  spam (1)
Yes: dict['key'] = list[index]
No:  dict ['key'] = list [index]

没有尾随空格。

在赋值 (=)、比较 (==, <, >, !=, <>, <=, >=, in, not in, is, is not) 和布尔运算符 (and, or, not) 的两侧各用一个空格包围二元运算符。 对于算术运算符 (+-*///%**@) 周围的空格插入,请自行判断。

Yes: x == 1
No:  x<1

传递关键字参数或定义默认参数值时,永远不要在 = 周围使用空格,但有一个例外:当存在类型注释时在默认参数值的 = 周围使用空格。

Yes: def complex(real, imag=0.0): return Magic(r=real, i=imag)
Yes: def complex(real, imag: float = 0.0): return Magic(r=real, i=imag)
No:  def complex(real, imag = 0.0): return Magic(r = real, i = imag)
No:  def complex(real, imag: float=0.0): return Magic(r = real, i = imag)

不要使用空格在连续的行上垂直对齐标记,因为它会成为维护负担(适用于 :#= 等)。

Yes:
  foo = 1000  # comment
  long_name = 2  # comment that should not be aligned

  dictionary = {
      'foo': 1,
      'long_name': 2,
  }
No:
  foo       = 1000  # comment
  long_name = 2     # comment that should not be aligned

  dictionary = {
      'foo'      : 1,
      'long_name': 2,
  }

3.7 Shebang 行

大多数 .py 文件不需要以 #! 行开头。 使用 #!/usr/bin/env python3 (以支持 virtualenvs)或 #!/usr/bin/python3 根据 PEP-394 启动程序的主文件。

内核使用此行来查找 Python 解释器,但在导入模块时 Python 会忽略它。 它仅在打算直接执行的文件上是必需的。

3.8 注释和文档字符串

确保为模块、函数、方法文档字符串和内联注释使用正确的样式。

3.8.1 文档字符串

Python 使用文档字符串来记录代码。 文档字符串是一个字符串,它是包、模块、类或函数中的第一个语句。 这些字符串可以通过对象的 __doc__ 成员自动提取,并由 pydoc 使用。 (尝试在你的模块上运行 pydoc,看看它的外观。)始终使用三个双引号 """ 格式表示文档字符串(根据 PEP 257)。 文档字符串应组织为摘要行(一个物理行不超过 80 个字符),并以句点、问号或感叹号结尾。 在编写更多内容(鼓励)时,这必须后跟一个空行,然后是文档字符串的其余部分,该部分从与第一行第一个引号相同的光标位置开始。 下面有更多关于文档字符串的格式化指南。

3.8.2 模块

每个文件都应包含许可证样板。 为项目使用的许可证选择合适的样板(例如,Apache 2.0、BSD、LGPL、GPL)。

文件应以描述模块内容和用法的文档字符串开头。

"""A one-line summary of the module or program, terminated by a period.

Leave one blank line.  The rest of this docstring should contain an
overall description of the module or program.  Optionally, it may also
contain a brief description of exported classes and functions and/or usage
examples.

Typical usage example:

  foo = ClassFoo()
  bar = foo.function_bar()
"""

3.8.2.1 测试模块

测试文件的模块级别文档字符串不是必需的。 只有在可以提供其他信息时才应包含它们。

示例包括有关如何运行测试的一些细节、对不寻常设置模式的解释、对外部环境的依赖等等。

"""This blaze test uses golden files.

You can update those files by running
`blaze run //foo/bar:foo_test -- --update_golden_files` from the `google3`
directory.
"""

不提供任何新信息的文档字符串不应使用。

"""Tests for foo.bar."""

3.8.3 函数和方法

在本节中,“函数”是指方法、函数、生成器或属性。

对于具有以下一个或多个属性的每个函数,文档字符串是强制性的

文档字符串应提供足够的信息,以便在不阅读函数代码的情况下编写对函数的调用。 文档字符串应描述函数的调用语法及其语义,但通常不描述其实现细节,除非这些细节与函数的使用方式相关。 例如,以副作用方式修改其参数之一的函数应在其文档字符串中注明这一点。 否则,函数实现的细微但重要的细节(与调用者无关)最好用代码旁边的注释而不是在函数的文档字符串中表达。

文档字符串可以是描述性的("""从 Bigtable 获取行。""")或命令式的("""从 Bigtable 获取行。"""),但样式在文件中应保持一致。 @property 数据描述符的文档字符串应使用与属性或 函数参数 的文档字符串相同的样式("""Bigtable 路径。""",而不是 """返回 Bigtable 路径。""")。

函数的某些方面应在特殊部分中记录,如下所示。 每个部分都以标题行开头,该行以冒号结尾。 除了标题之外的所有部分都应保持两个或四个空格的悬挂缩进(在文件中保持一致)。 如果函数的名称和签名信息足够,可以使用单行文档字符串恰当地描述它,则可以省略这些部分。

Args
按名称列出每个参数。 说明应跟在名称之后,并用冒号分隔,后跟空格或换行符。 如果描述太长而无法容纳在单个 80 个字符的行中,请使用比参数名称多 2 或 4 个空格的悬挂缩进(与文件中其余的文档字符串保持一致)。 如果代码不包含相应的类型注释,则描述应包括所需的类型。 如果函数接受 *foo (可变长度参数列表)和/或 **bar (任意关键字参数),则应将它们列为 *foo**bar
Returns: (或 Yields: 对于生成器)
描述返回值的语义,包括类型注释未提供的任何类型信息。 如果函数仅返回 None,则不需要此部分。 如果文档字符串以 “Return”、“Returns”、“Yield” 或 “Yields” 开头(例如,"""将 Bigtable 中的行作为字符串元组返回。"""并且开头句子足以描述返回值,也可以省略它。 不要模仿旧的 “NumPy 样式”(示例),它经常将元组返回值记录为好像它是具有单独名称的多个返回值(从未提及元组)。 相反,将此类返回值描述为:“Returns: 一个元组 (mat_a, mat_b),其中 mat_a 是 …,…”。 文档字符串中的辅助名称不一定与函数体中使用的任何内部名称相对应(因为它们不是 API 的一部分)。 如果函数使用 yield (是一个生成器),则 Yields: 部分应记录 next() 返回的对象,而不是调用评估为的生成器对象本身。
Raises
列出与接口相关的所有异常,后跟描述。 使用类似的异常名称 + 冒号 + 空格或换行符和悬挂缩进样式,如 Args: 中所述。 如果违反了文档字符串中指定的 API,则不应记录引发的异常(因为这会自相矛盾地使违反 API 的行为成为 API 的一部分)。
def fetch_smalltable_rows(
    table_handle: smalltable.Table,
    keys: Sequence[bytes | str],
    require_all_keys: bool = False,
) -> Mapping[bytes, tuple[str, ...]]:
    """Fetches rows from a Smalltable.

    Retrieves rows pertaining to the given keys from the Table instance
    represented by table_handle.  String keys will be UTF-8 encoded.

    Args:
        table_handle: An open smalltable.Table instance.
        keys: A sequence of strings representing the key of each table
          row to fetch.  String keys will be UTF-8 encoded.
        require_all_keys: If True only rows with values set for all keys will be
          returned.

    Returns:
        A dict mapping keys to the corresponding table row data
        fetched. Each row is represented as a tuple of strings. For
        example:

        {b'Serak': ('Rigel VII', 'Preparer'),
         b'Zim': ('Irk', 'Invader'),
         b'Lrrr': ('Omicron Persei 8', 'Emperor')}

        Returned keys are always bytes.  If a key from the keys argument is
        missing from the dictionary, then that row was not found in the
        table (and require_all_keys must have been False).

    Raises:
        IOError: An error occurred accessing the smalltable.
    """

同样,也允许使用这种带有换行符的 Args: 变体

def fetch_smalltable_rows(
    table_handle: smalltable.Table,
    keys: Sequence[bytes | str],
    require_all_keys: bool = False,
) -> Mapping[bytes, tuple[str, ...]]:
    """Fetches rows from a Smalltable.

    Retrieves rows pertaining to the given keys from the Table instance
    represented by table_handle.  String keys will be UTF-8 encoded.

    Args:
      table_handle:
        An open smalltable.Table instance.
      keys:
        A sequence of strings representing the key of each table row to
        fetch.  String keys will be UTF-8 encoded.
      require_all_keys:
        If True only rows with values set for all keys will be returned.

    Returns:
      A dict mapping keys to the corresponding table row data
      fetched. Each row is represented as a tuple of strings. For
      example:

      {b'Serak': ('Rigel VII', 'Preparer'),
       b'Zim': ('Irk', 'Invader'),
       b'Lrrr': ('Omicron Persei 8', 'Emperor')}

      Returned keys are always bytes.  If a key from the keys argument is
      missing from the dictionary, then that row was not found in the
      table (and require_all_keys must have been False).

    Raises:
      IOError: An error occurred accessing the smalltable.
    """

3.8.3.1 重写方法

如果方法使用 @override (来自 typing_extensionstyping 模块)显式修饰,则覆盖基类的方法不需要文档字符串,除非覆盖方法的行为实质上改进了基方法的约定,或者需要提供详细信息(例如,记录额外的副作用),在这种情况下,覆盖方法上需要至少包含这些差异的文档字符串。

from typing_extensions import override

class Parent:
  def do_something(self):
    """Parent method, includes docstring."""

# Child class, method annotated with override.
class Child(Parent):
  @override
  def do_something(self):
    pass
# Child class, but without @override decorator, a docstring is required.
class Child(Parent):
  def do_something(self):
    pass

# Docstring is trivial, @override is sufficient to indicate that docs can be
# found in the base class.
class Child(Parent):
  @override
  def do_something(self):
    """See base class."""

3.8.4 类

类应在类定义下方有一个文档字符串,描述该类。 公共属性(不包括属性)应在此处在 Attributes 部分中记录,并遵循与 函数的 Args 部分相同的格式。

class SampleClass:
    """Summary of class here.

    Longer class information...
    Longer class information...

    Attributes:
        likes_spam: A boolean indicating if we like SPAM or not.
        eggs: An integer count of the eggs we have laid.
    """

    def __init__(self, likes_spam: bool = False):
        """Initializes the instance based on spam preference.

        Args:
          likes_spam: Defines if instance exhibits this preference.
        """
        self.likes_spam = likes_spam
        self.eggs = 0

    @property
    def butter_sticks(self) -> int:
        """The number of butter sticks we have."""

所有类文档字符串都应以描述类实例表示的内容的单行摘要开头。 这意味着 Exception 的子类也应描述异常表示的内容,而不是可能发生异常的上下文。 类文档字符串不应重复不必要的信息,例如类是一个类。

# Yes:
class CheeseShopAddress:
  """The address of a cheese shop.

  ...
  """

class OutOfCheeseError(Exception):
  """No more cheese is available."""
# No:
class CheeseShopAddress:
  """Class that describes the address of a cheese shop.

  ...
  """

class OutOfCheeseError(Exception):
  """Raised when no more cheese is available."""

3.8.5 块和行内注释

最后一个添加注释的位置是在代码的棘手部分。 如果你需要在下一次 代码审查 中解释它,你应该现在注释它。 复杂的操作在操作开始之前会得到几行注释。 不明显的会在行尾添加注释。

# We use a weighted dictionary search to find out where i is in
# the array.  We extrapolate position based on the largest num
# in the array and the array size and then do binary search to
# get the exact number.

if i & (i-1) == 0:  # True if i is 0 or a power of 2.

为了提高可读性,这些注释应该从代码开始至少空 2 个空格,然后是注释字符 #,之后至少空 1 个空格,再接注释文本本身。

另一方面,永远不要描述代码。假设阅读代码的人比你更懂 Python(虽然他们不知道你想要做什么)。

# BAD COMMENT: Now go through the b array and make sure whenever i occurs
# the next element is i+1

3.8.6 标点符号、拼写和语法

注意标点符号、拼写和语法;编写良好的注释比编写糟糕的注释更容易阅读。

注释应该像叙述性文本一样可读,并带有适当的大写和标点符号。在许多情况下,完整的句子比句子片段更易于阅读。较短的注释,例如在代码行末尾的注释,有时可以不那么正式,但你应该保持风格一致。

虽然代码审查员指出你该用分号却用了逗号会让人沮丧,但源代码保持高度清晰和可读性非常重要。正确的标点符号、拼写和语法有助于实现这一目标。

3.10 字符串

使用 f-string% 运算符或 format 方法来格式化字符串,即使参数都是字符串。根据你的最佳判断来选择字符串格式化选项。单个使用 + 的 join 操作是可以的,但不要使用 + 进行格式化。

Yes: x = f'name: {name}; score: {n}'
     x = '%s, %s!' % (imperative, expletive)
     x = '{}, {}'.format(first, second)
     x = 'name: %s; score: %d' % (name, n)
     x = 'name: %(name)s; score: %(score)d' % {'name':name, 'score':n}
     x = 'name: {}; score: {}'.format(name, n)
     x = a + b
No: x = first + ', ' + second
    x = 'name: ' + name + '; score: ' + str(n)

避免在循环中使用 ++= 运算符来累积字符串。在某些情况下,通过加法累积字符串可能会导致二次方而不是线性运行时间。虽然 CPython 可能会优化此类常见累积,但这只是一个实现细节。应用优化的条件不容易预测,并且可能会发生变化。相反,将每个子字符串添加到列表中,并在循环结束后使用 ''.join 连接列表,或者将每个子字符串写入 io.StringIO 缓冲区。这些技术始终具有分摊的线性运行时间复杂度。

Yes: items = ['<table>']
     for last_name, first_name in employee_list:
         items.append('<tr><td>%s, %s</td></tr>' % (last_name, first_name))
     items.append('</table>')
     employee_table = ''.join(items)
No: employee_table = '<table>'
    for last_name, first_name in employee_list:
        employee_table += '<tr><td>%s, %s</td></tr>' % (last_name, first_name)
    employee_table += '</table>'

在一个文件中,保持字符串引号字符的选择一致。选择 '" 并坚持使用。可以使用另一个引号字符来避免在字符串中需要反斜杠转义引号字符。

Yes:
  Python('Why are you hiding your eyes?')
  Gollum("I'm scared of lint errors.")
  Narrator('"Good!" thought a happy Python reviewer.')
No:
  Python("Why are you hiding your eyes?")
  Gollum('The lint. It burns. It burns us.')
  Gollum("Always the great lint. Watching. Watching.")

对于多行字符串,优先使用 """ 而不是 '''。如果项目对所有非文档字符串的多行字符串都使用 ''',并且对常规字符串也使用 ',则是可以接受的。无论如何,文档字符串必须使用 """

多行字符串不随程序其余部分的缩进流动。如果需要避免在字符串中嵌入额外的空格,请使用串联的单行字符串或使用 textwrap.dedent() 的多行字符串,以删除每行的初始空格。

  No:
  long_string = """This is pretty ugly.
Don't do this.
"""
  Yes:
  long_string = """This is fine if your use case can accept
      extraneous leading spaces."""
  Yes:
  long_string = ("And this is fine if you cannot accept\n" +
                 "extraneous leading spaces.")
  Yes:
  long_string = ("And this too is fine if you cannot accept\n"
                 "extraneous leading spaces.")
  Yes:
  import textwrap

  long_string = textwrap.dedent("""\
      This is also fine, because textwrap.dedent()
      will collapse common leading spaces in each line.""")

请注意,在此处使用反斜杠并不违反禁止显式行继续的规定;在这种情况下,反斜杠是在字符串文字中转义换行符

3.10.1 日志

对于期望模式字符串(带有 % 占位符)作为其第一个参数的日志记录函数:始终使用字符串字面量(而不是 f-string!)作为它们的第一个参数,并将模式参数作为后续参数传递。一些日志记录实现将未扩展的模式字符串收集为可查询字段。它还可以防止花费时间渲染没有配置任何记录器来输出的消息。

  Yes:
  import tensorflow as tf
  logger = tf.get_logger()
  logger.info('TensorFlow Version is: %s', tf.__version__)
  Yes:
  import os
  from absl import logging

  logging.info('Current $PAGER is: %s', os.getenv('PAGER', default=''))

  homedir = os.getenv('HOME')
  if homedir is None or not os.access(homedir, os.W_OK):
    logging.error('Cannot write to home directory, $HOME=%r', homedir)
  No:
  import os
  from absl import logging

  logging.info('Current $PAGER is:')
  logging.info(os.getenv('PAGER', default=''))

  homedir = os.getenv('HOME')
  if homedir is None or not os.access(homedir, os.W_OK):
    logging.error(f'Cannot write to home directory, $HOME={homedir!r}')

3.10.2 错误消息

错误消息(例如:ValueError 等异常上的消息字符串,或向用户显示的消息)应遵循三个准则

  1. 消息需要精确匹配实际的错误情况。

  2. 插入的部分需要始终清晰地标识为插入的。

  3. 它们应该允许简单的自动化处理(例如 grepping)。

  Yes:
  if not 0 <= p <= 1:
    raise ValueError(f'Not a probability: {p=}')

  try:
    os.rmdir(workdir)
  except OSError as error:
    logging.warning('Could not remove directory (reason: %r): %r',
                    error, workdir)
  No:
  if p < 0 or p > 1:  # PROBLEM: also false for float('nan')!
    raise ValueError(f'Not a probability: {p=}')

  try:
    os.rmdir(workdir)
  except OSError:
    # PROBLEM: Message makes an assumption that might not be true:
    # Deletion might have failed for some other reason, misleading
    # whoever has to debug this.
    logging.warning('Directory already was deleted: %s', workdir)

  try:
    os.rmdir(workdir)
  except OSError:
    # PROBLEM: The message is harder to grep for than necessary, and
    # not universally non-confusing for all possible values of `workdir`.
    # Imagine someone calling a library function with such code
    # using a name such as workdir = 'deleted'. The warning would read:
    # "The deleted directory could not be deleted."
    logging.warning('The %s directory could not be deleted.', workdir)

3.11 文件、套接字和类似的具有状态的资源

完成文件和套接字的操作后,显式关闭它们。此规则自然地扩展到内部使用套接字的可关闭资源,例如数据库连接,以及其他需要以类似方式关闭的资源。仅举几个例子,这还包括 mmap 映射,h5py 文件对象matplotlib.pyplot 图形窗口

不必要地保持文件、套接字或其他此类有状态对象打开有很多缺点

此外,虽然文件和套接字(以及一些类似行为的资源)在对象被销毁时会自动关闭,但将对象的生命周期与资源的状态耦合起来是不良做法

一次又一次地发现,依靠终结器来进行具有可观察副作用的自动清理会导致重大问题,这经历了数十年和多种语言(例如,请参阅 这篇文章 了解 Java)。

管理文件和类似资源的首选方法是使用 with 语句

with open("hello.txt") as hello_file:
    for line in hello_file:
        print(line)

对于不支持 with 语句的类文件对象,请使用 contextlib.closing()

import contextlib

with contextlib.closing(urllib.urlopen("https://pythonlang.cn/")) as front_page:
    for line in front_page:
        print(line)

在基于上下文的资源管理不可行的极少数情况下,代码文档必须清楚地解释如何管理资源生命周期。

3.12 TODO 注释

对于临时代码、短期解决方案或足够好但不完美的代码,请使用 TODO 注释。

TODO 注释以全部大写的单词 TODO 开头,后跟冒号,以及指向包含上下文的资源的链接,最好是 bug 引用。bug 引用是首选的,因为 bug 会被跟踪并具有后续注释。在此上下文之后,添加一个用连字符 - 引出的解释性字符串。目的是拥有一个一致的 TODO 格式,可以搜索以找出如何获取更多详细信息。

# TODO: crbug.com/192795 - Investigate cpufreq optimizations.

旧样式,以前推荐,但不鼓励在新代码中使用

# TODO(crbug.com/192795): Investigate cpufreq optimizations.
# TODO(yourusername): Use a "\*" here for concatenation operator.

避免添加将个人或团队作为上下文的 TODO。

# TODO: @yourusername - File an issue and use a '*' for repetition.

如果你的 TODO 形式为“在未来某个日期做某事”,请确保你包含一个非常具体的日期(“在 2009 年 11 月之前修复”)或一个非常具体的事件(“当所有客户端都可以处理 XML 响应时删除此代码”),未来的代码维护者将会理解。Issue 非常适合跟踪此事。

3.13 导入格式

导入语句应位于单独的行上;对于 typingcollections.abc 导入存在例外情况。

例如:

Yes: from collections.abc import Mapping, Sequence
     import os
     import sys
     from typing import Any, NewType
No:  import os, sys

导入语句始终放在文件的顶部,紧接在任何模块注释和文档字符串之后,以及模块全局变量和常量之前。 导入应从最通用到最不通用分组

  1. Python future 导入语句。例如

    from __future__ import annotations
    

    有关这些的更多信息,请参见上文

  2. Python 标准库导入。例如

    import sys
    
  3. 第三方模块或包导入。例如

    import tensorflow as tf
    
  4. 代码库子包导入。例如

    from otherproject.ai import mind
    
  5. 已弃用:与此文件属于同一顶级子包的特定于应用程序的导入。例如

    from myproject.backend.hgwells import time_machine
    

    你可能会发现较旧的 Google Python 样式代码这样做,但不再需要这样做。鼓励新代码不要为此烦恼。 只需将特定于应用程序的子包导入与其他子包导入一样对待即可。

在每个分组中,导入应该按照每个模块的完整包路径(from path import ... 中的 path)按字典顺序排序,忽略大小写。代码可以选择在导入部分之间放置一个空行。

import collections
import queue
import sys

from absl import app
from absl import flags
import bs4
import cryptography
import tensorflow as tf

from book.genres import scifi
from myproject.backend import huxley
from myproject.backend.hgwells import time_machine
from myproject.backend.state_machine import main_loop
from otherproject.ai import body
from otherproject.ai import mind
from otherproject.ai import soul

# Older style code may have these imports down here instead:
#from myproject.backend.hgwells import time_machine
#from myproject.backend.state_machine import main_loop

3.14 语句

通常每行只有一个语句。

但是,只有当整个语句适合一行时,你才能将测试的结果放在与测试同一行上。 特别是,你永远不能对 try/except 这样做,因为 tryexcept 不能同时适合同一行,并且你只能在没有 elseif 中这样做。

Yes:

  if foo: bar(foo)
No:

  if foo: bar(foo)
  else:   baz(foo)

  try:               bar(foo)
  except ValueError: baz(foo)

  try:
      bar(foo)
  except ValueError: baz(foo)

3.15 Getters 和 Setters

当 getter 和 setter 函数(也称为访问器和修改器)为获取或设置变量的值提供有意义的角色或行为时,应使用它们。

特别是,当获取或设置变量很复杂或成本很高时,无论是当前还是在合理的未来,都应该使用它们。

例如,如果一对 getter/setter 只是读取和写入内部属性,则应将内部属性公开。 相比之下,如果设置变量意味着某些状态无效或重建,则应使用 setter 函数。 函数调用暗示正在发生潜在的非平凡操作。 或者,当需要简单的逻辑时,属性可能是一个选项,或者重构为不再需要 getter 和 setter。

Getter 和 Setter 应该遵循 命名 指南,例如 get_foo()set_foo()

如果过去的行为允许通过属性访问,请不要将新的 getter/setter 函数绑定到该属性。 任何仍然尝试通过旧方法访问变量的代码都应该明显中断,以便让他们意识到复杂性的变化。

3.16 命名

module_name, package_name, ClassName, method_name, ExceptionName, function_name, GLOBAL_CONSTANT_NAME, global_var_name, instance_var_name, function_parameter_name, local_var_name, query_proper_noun_for_thing, send_acronym_via_https

名称应具有描述性。这包括函数、类、变量、属性、文件和任何其他类型的命名实体。

避免缩写。 特别是,不要使用那些含糊不清或项目之外的读者不熟悉的缩写,并且不要通过删除单词中的字母来缩写。

始终使用 .py 文件扩展名。 永远不要使用破折号。

3.16.1 应避免的名称

3.16.2 命名约定

3.16.3 文件命名

Python 文件名必须具有 .py 扩展名,并且不得包含破折号 (-)。 这允许导入和进行单元测试。 如果希望可执行文件在没有扩展名的情况下可访问,请使用符号链接或包含 exec "$0.py" "$@" 的简单 bash 包装器。

3.16.4 指南源自 Guido 的建议

类型 公共 内部
lower_with_under
模块 lower_with_under _lower_with_under
CapWords _CapWords
异常 CapWords
函数 lower_with_under() _lower_with_under()
全局/类 常量 CAPS_WITH_UNDER _CAPS_WITH_UNDER
全局/类 变量 lower_with_under _lower_with_under
实例变量 lower_with_under _lower_with_under (受保护)
方法名称 lower_with_under() _lower_with_under() (受保护)
函数/方法 参数 lower_with_under
局部变量 lower_with_under

3.16.5 数学符号

对于数学密集型代码,当它们与参考论文或算法中已建立的符号匹配时,优先使用否则会违反样式指南的短变量名。

使用基于已建立符号的名称时

  1. 引用所有命名约定的来源,最好在注释或文档字符串中使用指向学术资源本身的超链接。 如果无法访问该来源,请清楚地记录命名约定。
  2. 对于公共 API,更喜欢符合 PEP8 的 descriptive_names,这些 API 更可能在上下文之外遇到。
  3. 使用范围狭窄的 pylint: disable=invalid-name 指令来消除警告。 对于少量变量,将指令用作每个变量的行尾注释;对于更多变量,在块的开头应用该指令。

3.17 Main

在 Python 中,pydoc 以及单元测试要求模块可导入。 如果文件旨在用作可执行文件,则其主要功能应在 main() 函数中,并且您的代码应始终在执行主程序之前检查 if __name__ == '__main__',这样在导入模块时就不会执行它。

使用 absl 时,使用 app.run

from absl import app
...

def main(argv: Sequence[str]):
    # process non-flag arguments
    ...

if __name__ == '__main__':
    app.run(main)

否则,请使用

def main():
    ...

if __name__ == '__main__':
    main()

当模块导入时,将执行顶层的所有代码。 注意不要调用函数、创建对象或执行其他不应在文件被 pydoced 时执行的操作。

3.18 函数长度

首选小型且集中的函数。

我们认识到长函数有时是合适的,因此对函数长度没有硬性限制。如果一个函数超过 40 行左右,请考虑是否可以在不损害程序结构的情况下将其分解。

即使你的长函数现在运行良好,几个月后修改它的人可能会添加新的行为。这可能会导致难以发现的错误。保持你的函数简短明了,可以使其他人更容易阅读和修改你的代码。

在使用某些代码时,你可能会发现长而复杂的函数。不要害怕修改现有代码:如果发现使用这样的函数很困难,发现错误很难调试,或者你想在几个不同的上下文中使用它的一部分,请考虑将该函数分解为更小且更易于管理的部分。

3.19 类型注解

3.19.1 通用规则

3.19.2 行断开

尽量遵循现有的缩进规则。

注释后,许多函数签名将变为“每行一个参数”。为了确保返回类型也获得其自己的行,可以在最后一个参数之后放置一个逗号。

def my_method(
    self,
    first_var: int,
    second_var: Foo,
    third_var: Bar | None,
) -> int:
  ...

始终首选在变量之间换行,而不是例如在变量名和类型注释之间换行。 但是,如果所有内容都适合在同一行上,请进行操作。

def my_method(self, first_var: int) -> int:
  ...

如果函数名称、最后一个参数和返回类型的组合太长,则在新行中缩进 4 个空格。使用换行符时,首选将每个参数和返回类型放在它们自己的行上,并将右括号与 def 对齐

Yes:
def my_method(
    self,
    other_arg: MyLongType | None,
) -> tuple[MyLongType1, MyLongType1]:
  ...

(可选)可以将返回类型放在与最后一个参数同一行上。

Okay:
def my_method(
    self,
    first_var: int,
    second_var: int) -> dict[OtherLongType, MyLongType]:
  ...

pylint 允许您将右括号移动到新的一行,并与左括号对齐,但这会降低可读性。

No:
def my_method(self,
              other_arg: MyLongType | None,
             ) -> dict[OtherLongType, MyLongType]:
  ...

如以上示例所示,尽量不要拆分类型。但是,有时类型太长,无法放在一行上(尽量保持子类型完整)。

def my_method(
    self,
    first_var: tuple[list[MyLongType1],
                     list[MyLongType2]],
    second_var: list[dict[
        MyLongType3, MyLongType4]],
) -> None:
  ...

如果单个名称和类型太长,请考虑使用 别名 表示该类型。最后的选择是在冒号后换行并缩进 4 个空格。

Yes:
def my_function(
    long_variable_name:
        long_module_name.LongTypeName,
) -> None:
  ...
No:
def my_function(
    long_variable_name: long_module_name.
        LongTypeName,
) -> None:
  ...

3.19.3 前向声明

如果您需要使用尚未定义的类名(来自同一模块)——例如,如果您需要在该类的声明中使用类名,或者您使用的类在代码后面定义——可以使用 from __future__ import annotations,或者使用字符串表示类名。

Yes:
from __future__ import annotations

class MyClass:
  def __init__(self, stack: Sequence[MyClass], item: OtherClass) -> None:

class OtherClass:
  ...
Yes:
class MyClass:
  def __init__(self, stack: Sequence['MyClass'], item: 'OtherClass') -> None:

class OtherClass:
  ...

3.19.4 默认值

根据 PEP-008 当参数同时具有类型注解和默认值时,才在 = 周围使用空格。

Yes:
def func(a: int = 0) -> int:
  ...
No:
def func(a:int=0) -> int:
  ...

3.19.5 NoneType

在 Python 类型系统中,NoneType 是一种“一等”类型,出于类型标注的目的,NoneNoneType 的别名。如果一个参数可以为 None,则必须声明它!您可以使用 | 联合类型表达式(推荐在新的 Python 3.10+ 代码中使用),或者使用较旧的 OptionalUnion 语法。

使用显式的 X | None 而不是隐式的。早期版本的类型检查器允许将 a: str = None 解释为 a: str | None = None,但现在不再是首选行为。

Yes:
def modern_or_union(a: str | int | None, b: str | None = None) -> str:
  ...
def union_optional(a: Union[str, int, None], b: Optional[str] = None) -> str:
  ...
No:
def nullable_union(a: Union[None, str]) -> str:
  ...
def implicit_optional(a: str = None) -> str:
  ...

3.19.6 类型别名

您可以声明复杂类型的别名。别名的名称应该使用 CapWords 命名法。如果别名仅在此模块中使用,则应使用 _Private 前缀。

请注意,: TypeAlias 注解仅在 3.10+ 版本中受支持。

from typing import TypeAlias

_LossAndGradient: TypeAlias = tuple[tf.Tensor, tf.Tensor]
ComplexTFMap: TypeAlias = Mapping[str, _LossAndGradient]

3.19.7 忽略类型

您可以使用特殊注释 # type: ignore 在一行上禁用类型检查。

pytype 具有针对特定错误的禁用选项(类似于 lint)。

# pytype: disable=attribute-error

3.19.8 类型变量

带注解的赋值
如果内部变量的类型难以或无法推断,请使用带注解的赋值来指定其类型 - 在变量名和值之间使用冒号和类型(与具有默认值的函数参数相同)。
a: Foo = SomeUndecoratedFunction()
类型注释
尽管您可能会看到它们仍然存在于代码库中(在 Python 3.6 之前是必需的),但不要在线尾添加更多的 # type: <type name> 注释。
a = SomeUndecoratedFunction()  # type: Foo

3.19.9 元组 vs 列表

类型化的列表只能包含单一类型的对象。类型化的元组可以具有单一重复类型或具有不同类型的固定数量的元素。后者通常用作函数的返回类型。

a: list[int] = [1, 2, 3]
b: tuple[int, ...] = (1, 2, 3)
c: tuple[int, str, float] = (1, "2", 3.5)

3.19.10 类型变量

Python 类型系统具有 泛型。类型变量(例如 TypeVarParamSpec)是使用它们的常用方法。

示例

from collections.abc import Callable
from typing import ParamSpec, TypeVar
_P = ParamSpec("_P")
_T = TypeVar("_T")
...
def next(l: list[_T]) -> _T:
  return l.pop()

def print_when_called(f: Callable[_P, _T]) -> Callable[_P, _T]:
  def inner(*args: _P.args, **kwargs: _P.kwargs) -> _T:
    print("Function was called")
    return f(*args, **kwargs)
  return inner

TypeVar 可以被约束。

AddableType = TypeVar("AddableType", int, float, str)
def add(a: AddableType, b: AddableType) -> AddableType:
  return a + b

typing 模块中常见的预定义类型变量是 AnyStr。将其用于可以为 bytesstr 并且必须都是相同类型的多个注解。

from typing import AnyStr
def check_length(x: AnyStr) -> AnyStr:
  if len(x) <= 42:
    return x
  raise ValueError()

类型变量必须具有描述性名称,除非它满足以下所有条件:

Yes:
  _T = TypeVar("_T")
  _P = ParamSpec("_P")
  AddableType = TypeVar("AddableType", int, float, str)
  AnyFunction = TypeVar("AnyFunction", bound=Callable)
No:
  T = TypeVar("T")
  P = ParamSpec("P")
  _T = TypeVar("_T", int, float, str)
  _F = TypeVar("_F", bound=Callable)

3.19.11 字符串类型

不要在新代码中使用 typing.Text。它仅用于 Python 2/3 兼容性。

对字符串/文本数据使用 str。对于处理二进制数据的代码,使用 bytes

def deals_with_text_data(x: str) -> str:
  ...
def deals_with_binary_data(x: bytes) -> bytes:
  ...

如果函数的所有字符串类型始终相同,例如,如果返回类型与上面代码中的参数类型相同,请使用 AnyStr

3.19.12 用于类型注解的导入

对于来自 typingcollections.abc 模块的符号(包括类型、函数和常量),用于支持静态分析和类型检查,始终导入符号本身。这使常见的注解更加简洁,并与世界各地使用的类型标注实践相匹配。您明确允许在一行中从 typingcollections.abc 模块导入多个特定符号。例如:

from collections.abc import Mapping, Sequence
from typing import Any, Generic, cast, TYPE_CHECKING

鉴于这种导入方式会将项目添加到本地命名空间,因此应将 typingcollections.abc 中的名称视为类似于关键字,并且不应在 Python 代码(无论是否类型化)中定义。如果类型与模块中的现有名称之间存在冲突,请使用 import x as y 导入它。

from typing import Any as AnyType

在注解函数签名时,首选抽象容器类型(如 collections.abc.Sequence)而不是具体类型(如 list)。如果您需要使用具体类型(例如,类型化的元素的 tuple),则首选内置类型(如 tuple)而不是 typing 模块中的参数化类型别名(例如,typing.Tuple)。

from typing import List, Tuple

def transform_coordinates(original: List[Tuple[float, float]]) ->
    List[Tuple[float, float]]:
  ...
from collections.abc import Sequence

def transform_coordinates(original: Sequence[tuple[float, float]]) ->
    Sequence[tuple[float, float]]:
  ...

3.19.13 条件导入

仅在特殊情况下使用条件导入,即必须在运行时避免类型检查所需的额外导入。不鼓励使用此模式;应首选重构代码以允许顶级导入等替代方案。

仅用于类型注解的导入可以放在 if TYPE_CHECKING: 块中。

3.19.14 循环依赖

由类型标注引起的循环依赖关系是代码异味。此类代码是重构的好选择。虽然从技术上讲可以保持循环依赖关系,但各种构建系统都不会允许您这样做,因为每个模块都必须依赖于另一个模块。

将创建循环依赖项导入的模块替换为 Any。使用有意义的名称设置 别名,并使用来自此模块的真实类型名称(Any 的任何属性都是 Any)。别名定义应与最后一个导入之间用一行分隔。

from typing import Any

some_mod = Any  # some_mod.py imports this module.
...

def my_method(self, var: "some_mod.SomeType") -> None:
  ...

3.19.15 泛型

在注解时,首选在参数列表中为 泛型类型指定类型参数;否则,泛型的参数将被假定为 Any

# Yes:
def get_names(employee_ids: Sequence[int]) -> Mapping[int, str]:
  ...
# No:
# This is interpreted as get_names(employee_ids: Sequence[Any]) -> Mapping[Any, Any]
def get_names(employee_ids: Sequence) -> Mapping:
  ...

如果泛型的最佳类型参数是 Any,请使其显式化,但请记住,在许多情况下,TypeVar 可能更合适。

# No:
def get_names(employee_ids: Sequence[Any]) -> Mapping[Any, str]:
  """Returns a mapping from employee ID to employee name for given IDs."""
# Yes:
_T = TypeVar('_T')
def get_names(employee_ids: Sequence[_T]) -> Mapping[_T, str]:
  """Returns a mapping from employee ID to employee name for given IDs."""

4 结束语

保持一致.

如果您正在编辑代码,请花几分钟时间查看周围的代码并确定其风格。如果他们在索引变量名中使用 _idx 后缀,您也应该这样做。如果他们的注释周围有小方框的井号,请让您的注释周围也有小方框的井号。

制定风格指南的目的是为了拥有一种通用的编码词汇表,以便人们可以专注于您所说的内容,而不是您如何表达它。我们在这里介绍全局样式规则,以便人们了解词汇表,但本地样式也很重要。如果您添加到文件的代码与周围的现有代码截然不同,那么当读者阅读它时,会让他们失去节奏。

但是,一致性是有局限性的。它更多地适用于本地以及全局样式未指定的选择。通常不应将一致性作为一种理由来以旧样式做事,而不考虑新样式的优点,或者代码库随着时间推移趋向于更新样式的趋势。