[styleguide]

谷歌Python编程风格指南

目录

  • 1 背景

  • 2 Python语言规则

    • 2.1 代码静态分析

    • 2.2 导入

    • 2.3 包

    • 2.4 异常

    • 2.5 可变的全局状态

    • 2.6 嵌套/局部/内部的类和函数

    • 2.7 推导式和生成器表达式

    • 2.8 迭代器和默认操作符

    • 2.9 生成器

    • 2.10 Lambda函数

    • 2.11 条件表达式

    • 2.12 默认参数值

    • 2.13 属性

    • 2.14 判真/判假

    • 2.16 词法作用域

    • 2.17 函数和方法装饰器

    • 2.18 线程

    • 2.19 功能强大的新特性

    • 2.20 现代Python:使用__future__引入新特性

    • 2.21 类型注解代码

  • 3 Python风格规则

    • 3.1 分号

    • 3.2 行长度

    • 3.3 括号

    • 3.4 缩进

      • 3.4.1 序列中是否使用尾逗号?

    • 3.5 空行

    • 3.6 空格

    • 3.7 Shebang行

    • 3.8 注释和文档字符串

      • 3.8.1 文档字符串

      • 3.8.2 模块

      • 3.8.2.1 测试模块

      • 3.8.3 函数和方法

      • 3.8.4 类

      • 3.8.5 块和行内注释

      • 3.8.6 标点符号、拼写和语法

    • 3.10 字符串

      • 3.10.1 记录日志

      • 3.10.2 错误消息

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

    • 3.12 待办事项注释

    • 3.13 导入格式化

    • 3.14 语句

    • 3.15 访问器

    • 3.16 命名

      • 3.16.1 避免使用的名称

      • 3.16.2 命名约定

      • 3.16.3 文件命名

      • 3.16.4 从指南推荐获得的指导方针

    • 3.17 主要部分

    • 3.18 函数长度

    • 3.19 类型注释

      • 3.19.1 通用规则

      • 3.19.2 换行

      • 3.19.3 前向引用

      • 3.19.4 默认值

      • 3.19.5 None类型

      • 3.19.6 类型别名

      • 3.19.7 忽略类型

      • 3.19.8 编写类型变量

      • 3.19.9 元组 vs 列表

      • 3.19.10 类型变量

      • 3.19.11 字符串类型

      • 3.19.12 类型标记导入

      • 3.19.13 条件导入

      • 3.19.14 循环依赖

      • 3.19.15 泛型

      • 3.19.16 构建依赖项

  • 4 结语

1 背景

Python 是谷歌主要使用的动态语言。本编程风格指南是 Python 程序的 DOS 和 DON'TS 名单。

为了帮助您正确地格式化代码,我们创建了用于 Vim 的设置文件。对于 Emacs,默认设置应该是可以的。

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

2 Python语言规则

2.1 代码静态分析

使用 pylint 对您的代码进行代码静态分析并应用此文件中的指南。

2.1.1 定义

pylint是一种用于查找Python源码中的错误和风格问题的工具,它可以发现通常由像C和C++这样的不太动态的编程语言编译器捕获的问题。由于Python的动态性质,可能会产生错误的警告,但不应该出现错误的警告很少。

2.1.2 优点

能够捕获易被忽视的错误,比如拼写错误、在分配之前使用变量等。

2.1.3 缺点

pylint并不完美,有时我们需要迂回处理、禁止它的警告或修复它。

2.1.4 决策

确保使用 pylint 对您的代码进行代码静态分析。

如果警告不合适,则应禁用警告,以便其他问题不会被隐藏。要禁用警告,可以设置行级注释:

pylint警告由符号名称(例如:empty-docstring)标识,谷歌特定的警告以 g- 开头。

如果符号名称的原因不明确,则需要添加解释。

以这种方式禁用具有优点,我们可以轻松查找禁用并重新考虑它们。

您可以通过以下方式获得pylint警告列表:

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

使用 pylint: disable 而不是被弃用的旧形式 pylint: disable-msg

未使用的参数警告可以通过删除函数开头的变量来禁止。始终包括一个解释,说明为什么要将其删除。使用 "未使用" 即可。例如:

其他抑制此警告的常见形式包括将 " _ " 用作未使用参数的标识符,或在参数名前加上 " unused_ ",或将其赋值给 "_"。这些形式是允许的,但不再被鼓励。这些将打破按名称传递参数的调用者,也不强制执行确实未使用这些参数。

2.2 导入

只为包和模块使用 import 语句,不要为单个类或函数使用。

一种重复使用机制,用于从一个模块共享代码到另一个模块。

2.2.2 优点

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

2.2.3 缺点

模块名称仍可能冲突。某些模块名称太长难用。

2.2.4 决策

  • 在导入包和模块时使用 import x

  • 在以下情况下使用 from x import yx 是包前缀,y 是没有前缀的模块名称。

  • 在以下任一情况下使用 from x import y as z

    • 导入了两个名为 y 的模块。

    • y 与当前模块中定义的顶级名称冲突。

    • y 与公共 API 的一部分构成的常见参数名称发生冲突(例如 features)。

    • y 是一个名字太长的名称。

  • 仅在 z 是标准缩写语(例如 numpynp)时使用 import y as z

例如,模块 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 决策

所有新代码应使用其完整包名称导入每个模块。

导入应如下所示:

(假设此文件位于 doctor/who/,同时 jodie.py 同样在该路径下)

但不能假定主要二进制文件所在的目录在 sys.path 中,尽管在某些环境中是这样的。因此,代码应假定 import jodie 引用的是名为 jodie 的第三方或顶级包,而不是本地的 jodie.py

2.4 异常

异常是打破正常控制流以处理错误或其他异常情况的手段。

2.4.2 优点

正常操作代码的控制流不会因错误处理代码而混乱。还允许控制流在某些条件发生时跳过多个帧,例如在一步中从 N 嵌套函数中返回,而无需通过传递错误代码来实现。

2.4.3 缺点

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

2.4.4 决策

异常必须遵循以下条件:

  • 在有意义时使用内置的异常类。例如,引发 ValueError 表示编程错误,例如违反先决条件(例如,如果传递了负数但需要正数)。不要使用 assert 语句来验证公共 API 的参数值。assert 用于确保内部正确性,而不是强制正确的使用或指示发生了某些意外情况。如果后者需要异常,则请使用 raise 语句。例如:

    def connect_to_next_port(self, minimum: int) -> int: """连接到下一个可用端口。

    def connect_to_next_port(self, minimum: int) -> int: """Connects to the next available port.

  • 库或包可以定义自己的异常。这样做时它们必须从现有的异常类继承。异常名称应以 Error 结尾,不应引入重复(foo.FooError)。

  • 永远不要使用全捕获的 except: 语句或捕获 ExceptionStandardError,除非:

    • 重新引发异常,或

    • 创建一个程序中的隔离点,其中异常不会传播,而是记录和抑制,例如通过保护其最外层块来保护线程免受崩溃。

Python在这个方面非常宽容,except:将真正地捕捉到任何异常,包括拼写错误的名称、sys.exit()调用、Ctrl+C中断、单元测试失败以及各种您不希望捕获的异常。

  • 最小化在try/except块中的代码量。try体越大,越有可能由于你没预料到的代码行引发异常。在这种情况下,try/except块会隐藏一个真正的错误。

  • 使用finally子句来执行代码,无论try块中是否引发异常。这通常对于清理很有用,例如关闭一个文件。

2.5 可变全局状态

避免使用可变全局状态。

2.5.1 定义

模块级别的值或可以在程序执行期间得到修改的类属性。

2.5.2 优点

偶尔有些用处。

2.5.3 缺点

  • 打破了封装性:这种设计可能会使得实现有效目标变得困难。例如,如果全局状态用于管理数据库连接,那么同时连接到两个不同的数据库(如在迁移过程中计算差异)将变得困难。类似的问题也容易在全局注册表中出现。

  • 有可能在导入时改变模块行为,因为对全局变量的赋值是在首次导入模块时完成的。

2.5.4 决策

避免使用可变全局状态。

在那些罕见情况下,使用全局状态是有必要的,可变全局实体应该被声明为模块级别或类属性,并通过将名称前缀添加_来使其内部化。如果必要,访问可变全局状态的外部访问必须通过公共函数或类方法完成。请在注释或链接到注释的文档中解释设计原因为何使用可变全局状态。

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

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

嵌套的本地函数或类在用于关闭局部变量时可以使用。内部类也是可以的。

2.6.1 定义

类可以在方法、函数或类内定义。函数可以在方法或函数内定义。嵌套函数对于在封闭作用域中定义的变量只具有只读访问权限。

2.6.2 优点

允许定义只在非常有限的范围内使用的实用工具类和函数。非常像ADT。通常用于实现装饰器。

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子句,过滤器表达式。不允许使用多个for子句或过滤器表达式。当情况变得更复杂时,请使用循环。

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

​ ​ 否: result = [complicated_transform( x, some_argument=x+1) for x in iterable if predicate(x)]

2.8 默认迭代器和运算符

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

2.8.1 定义

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

2.8.2 优点

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

2.8.3 缺点

你不能通过阅读方法名称来判断对象的类型(除非变量具有类型注释)。这也是一个优势。

2.8.4 决策

使用默认的迭代器和类型支持的运算符,比如列表、字典和文件。这些内置类型也定义了迭代器方法。应当优先使用这些方法,而不是返回列表的方法,除非在迭代容器时不要改变容器。

​ 不适用: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 函数

适用于一行代码。应优先使用 map()filter()lambda 不同的是,最好使用生成器表达式。

2.10.1 定义

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

2.10.2 优点

方便。

2.10.3 缺点

比本地函数更难阅读和调试。由于缺少名称,堆栈跟踪更难以理解。由于函数只能包含一个表达式,因此表达能力受到限制。

2.10.4 决定

可以使用它们来进行一行代码的操作。如果 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 决定

适用于简单情况。每个部分都必须适合在一行上:

在代码更复杂的情况下,请使用完整的 if 语句。

例如:

2.12 默认参数值

在大多数情况下是适用的。

2.12.1 定义

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

2.12.2 优点

通常情况下,你的函数会使用大量默认值,但在偶尔的情况下,你想覆盖它们。默认参数值提供了一种易于实现此操作的方法,而不必为罕见的异常情况定义许多函数。由于 Python 不支持重载方法/函数,因此默认参数是模仿重载行为的一种简便方法。

2.12.3 缺点

默认参数在模块加载时仅计算一次。如果参数是可变对象,如列表或字典,则可能会引起问题。如果函数修改对象(例如,通过将项附加到列表),则会修改默认值。

2.12.4 决定

可以在函数或方法定义中使用默认参数值,但需注意以下细节:

不要将可变对象用作函数或方法定义的默认值。

例如:

2.13 属性

在大多数情况下是适用的。

属性可以用于控制需要微不足道的计算或逻辑来获取或设置属性。 属性实现必须符合常规属性访问的一般期望:它们是廉价的,直面的,和不会令人感到意外的。

2.13.1 定义

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

2.13.2 优点

  • 允许属性访问和赋值API,而不是使用getter和setter方法调用。

  • 可用于使属性只读。

  • 可以使计算惰性计算。

  • 提供了一种在类型内部独立于类用户进化的情况下维护类的公共接口的方法。

2.13.3 缺点

  • 可以像操作符重载一样隐藏副作用。

  • 可能对子类造成困惑。

2.13.4 决定

允许使用属性,但是,就像操作符重载一样,只有在必要时才应使用,符合典型属性访问的期望;否则遵循getter和setter规则。

例如,使用属性仅获取和设置内部属性是不允许的:没有发生任何计算,因此属性是不必要的(而是将属性公开)。 相比之下,允许使用属性来控制属性访问或计算基础值:逻辑简单且不会令人感到意外。

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

使用属性的继承可能不明显。不要使用属性来执行子类可能要覆盖和扩展的计算。

2.14 真/假 判断

尽量使用“隐式” False。

2.14.1 定义

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

2.14.2 优点

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

2.14.3 缺点

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

2.14.4 决定

如果可能,请使用“隐式” False,例如使用if foo:而不是if foo!= []:。 但是,您应该谨记以下几点:

  • 始终使用if foo is None:(或is not None)来检查None值。例如,在测试是否将默认为'None'的变量或参数设置为其他值时。 另一个值可能是在布尔上下文中为false的值!

  • 不要使用'=='将布尔变量与'False'进行比较。使用'if not x:'代替。如果需要区分“False”和“None”,则链接表达式,例如'if not x和x不是None:'。

  • 对于序列(字符串,列表,元组),使用空序列是false这一事实,所以if seq:if not seq:if len(seq):if not len(seq):更可取。

  • 在处理整数时,隐式的False可能涉及更多的风险(即意外将“None”处理为0)。可以将已知为整数(并非'len()'的结果)的值与整数0进行比较。

  • 请注意,“0”(即字符串0)评估为true。

  • 请注意,Numpy数组可能会在隐式布尔上下文中引发异常。在测试“np.array”的空时,请首选“.size”属性(例如,如果不使用用户,请使用“if not users.size”)。

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

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.3 缺点

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

2.17.4 决策

在存在明显优势时慎用装饰器。装饰器应遵循与函数相同的导入和命名指南。装饰器 pydoc 应清楚地说明该函数是一个装饰器。为装饰器编写单元测试。

避免在装饰器本身中使用外部依赖项(例如,不要依赖于文件、插座、数据库连接等),因为它们在装饰器运行时(可能是从pydoc或其他工具中导入时)可能无法使用。对于使用有效参数调用的装饰器,应尽量保证在所有情况下都成功。

装饰器是 ��top-level code�� 的一种特殊情况——请参见主要讨论。

除了被迫与现有库定义的API集成之外,永远不要使用 staticmethod,而是编写一个模块级别的函数。

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

2.18 线程

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

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

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

2.19 强大的功能

避免使用这些功能。

2.19.1 定义

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

2.19.2 优点

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

2.19.3 缺点

当它们不是绝对必要时,使用这些 ��cool �� 功能非常诱人。正在使用不寻常的功能底层的代码更难阅读、理解和调试。原始作者看起来似乎不是这样,但当重新访问代码时,比长但直截了当的代码更难。

2.19.4 决策

避免在您的代码中使用这些功能。

在标准库模块和类中,可以内部使用这些功能(例如 abc.ABCMetadataclassesenum)。

2.20 现代 Python:来自 future 的导入

新的语言版本语义变化可能会通过特殊的未来导入在更早的运行时基础上启用它们。

2.20.1 定义

通过 from __future__ import 语句启用更多现代特性,可以在预期的未来 Python 版本中提前使用这些特性。

2.20.2 优点

这已被证明可以使运行时版本升级更加平滑,因为可以在代码中声明兼容性并防止回归。现代代码更易于维护,因为它不太可能积累技术债务,在未来的运行时升级中会成为问题。

2.20.3 缺点

这样的代码可能无法在非常旧的解释器版本上工作,而这些版本引入了所需的未来语句。在支持大量环境的项目中,这种需要更为常见。

2.20.4 决策

来自 future 的导入

鼓励使用 from __future__ import 语句。它使得给定的源文件能够开始使用更现代的Python语法特性。一旦不再需要在版本中运行隐藏在 __future__ 导入后面的特性的版本上,可以随时删除这些行。

在可能执行旧版本的代码中,使用:

​ from future import generator_stop

以实现版本到3.5之前和3.7之后的平滑切换。有关更多信息,请阅读 Python future statement definitions

在您对代码仅在足够现代的环境中使用有信心之前,请不要删除这些导入。即使您今天没有在代码中使用特定未来导入启用的功能,但将其保留在文件中可以防止以后修改代码无意中依赖旧版本的行为。

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

2.21 类型注释代码

你可以按照 PEP-484 的规定,对 Python 代码进行类型提示,并使用像 pytype 这样的类型检查工具在构建时检查代码。

类型注释可以在源代码或 stub pyi 文件 中进行。在可能的情况下,注释应该在源代码中。对于第三方或扩展模块,请使用 pyi 文件。

2.21.1 定义

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

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

你还可以使用类似的 PEP-526 语法声明变量的类型:

​ a: SomeType = some_func()

2.21.2 优点

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

2.21.3 缺点

您必须保持类型声明的最新状态。您可能会看到类型错误,您认为这是有效的代码。使用[type checker] (https://github.com/google/pytype) 可能会减少您使用强大功能的能力。

2.21.4 决策

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

3 Python样式规则

3.1 分号

不要以分号结尾,在同一行上不要使用分号来放置两个语句。

3.2 行长

最大行长为_80个字符_。

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

  • 长导入语句。

  • URL、路径名或注释中的长标志。

  • 不包含分隔开的空格的长字符串模块级常量,例如URL或路径名。

  • Pylint禁用注释。 (例如:#pylint:disable=invalid-name

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

相反,利用Python的括号、括号和花括号内部的隐式行连接。如果需要,您可以在表达式周围添加额外的括号。

请注意,此规则不禁止在字符串内使用反斜杠换行符(请参见下文)。

当文字字符串不适合单个行时,请使用括号进行隐式行连接。

在可能的最高语法级别处中断行。如果必须将一行分两次,就在两次中都使用相同的语法级别进行中断。

在注释中,如有必要,请将长URL放在自己的行上。

请注意,上述行连续示例中元素的缩进方式。有关说明,请参见缩进部分。

在所有其他情况下,如果[Black](https://github.com/psf/black)或[Pyink](https://github.com/google/pyink)自动格式化程序无法帮助将行带到下限,则允许该行超过此最大值。当这是合理的时,建议作者根据上述说明手动分割行。

3.3 括号

节约使用括号。

在元组周围使用括号是可以的,但不是必需的。在返回语句或条件语句中不要使用它们,除非使用括号进行隐含行继续或指示元组。

3.4 缩进

使用_4个空格_缩进您的代码块。

不要使用制表符。隐式行继续应在垂直地对齐包装元素(请参见行长示例)或使用悬挂的4个空格缩进。关闭(圆形、方形或大括号)括号可以放置在表达式的末尾,也可以放置在单独的行上,但然后应该与相应的开放括号的行的缩进相同。

你是一名专业的学术论文翻译人员。

3.4.1 序列中的尾逗号

只有在包含元素的结束容器标记], )}不出现在同一行时, 建议在项目序列中使用尾逗号。尾逗号的存在也被用作我们的Python代码自动格式化器的提示。

3.7 Shebang Line

大多数.py文件不需要以#!行开头。程序的主文件应以#!/usr/bin/env python3(以支持虚拟环境)或#!/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)。

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

​ """模块或程序的一个一行总结,以句点结尾。

3.8.2.1 测试模块

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

例如,包括一些关于如何运行测试的具体信息,关于不寻常的设置模式的说明,依赖于外部环境等。

​ """这里不去模糊,拯救睡眠,灭胶囊药。"""

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

​ """ foo.bar测试。"""

3.8.3 函数和方法

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

对于具有以下一个或多个属性的每个函数,需要文档字符串:

  • 是公共 API 的一部分

  • 非微不足道的大小

  • 非常显然的逻辑

文档字符串应提供足够的信息来编写调用该函数的调用,而不需要阅读该函数的代码。文档字符串应描述函数的调用语法和语义,但通常不描述实现细节,除非这些细节与如何使用该函数相关。例如,在具有副作用的函数中使其参数之一发生突变应在其文档字符串中注明。否则,不相关于调用者的函数实现的微妙但重要的细节最好作为代码旁注释来表示,而不是函数的文档字符串。

文档字符串可以是描写性风格( """获取大表行。""")或命令性风格( """从Bigtable获取行。"""),但在文件中应是一致的。一个属性数据描述符的文档字符串应该和属性或函数参数的文档字符串使用相同的风格("""The Bigtable path.""" 而不是 """返回Bigtable路径。""")。

覆盖基类的方法的方法可以有一个简单的文档字符串,将读者发送到其被覆盖的方法的文档字符串,例如 """见基类"""。其原理是在许多地方重复在基本方法的文档字符串中已经存在的文档的需要不存在。然而,如果覆盖方法的行为与被覆盖的方法有实质性不同,或需要提供细节(例如,记录附加副作用),则需要在覆盖方法上至少使用这些差异文档字符串。

函数的特定方面应该在特殊部分中记录,下面列出这些部分。每个部分都以标题行开头,后跟冒号。除标题之外的所有部分都应保持两个或四个空格的悬挂缩进(在文件中保持一致)。如果函数的名称和签名已足够明确,以便可以使用单行docstring恰当地描述它,则可以省略这些部分。 Args

按名称列出每个参数。名称后面应跟随一个描述,用冒号后跟一个空格或新行分隔。如果描述太长而无法适合单个80个字符的行中,则使用缩进的悬挂缩进,比参数名称多2个或4个空格(与文件中的其余docstring保持一致)。如果代码不包含相应的类型注释,则描述应包括所需的类型(s) 。如果函数接受* foo(可变长度参数列表)和/或** bar(任意关键字参数),则应将它们列为* foo和** bar。

Returns(或_Yields_适用于生成器)

描述返回值的类型和含义。如果函数仅返回None,则不需要此部分。如果docstring以Return或Yields开头(例如,"""Returns row from Bigtable as a tuple of strings."""),并且开头的句子足以描述返回值,则可以省略此部分。不要模仿“NumPy风格”(示例),它经常将元组返回值记录为具有单独名称的多个返回值(从不提到元组)。相反,将这样的返回值描述为:“返回:元组(mat_a,mat_b),其中mat_a是,dengdeng。” docstring中的辅助名称不一定要对应于函数体中使用的任何内部名称(因为这些名称不是API的一部分)。 Raises

列出与接口相关的所有异常,后跟一个描述。与_Args_中描述的异常名称+冒号+空格或新行和悬挂缩进样式相同。不应记录违反docstring中指定的API而引发的异常(因为这会矛盾地使得在API违反情况下的行为成为API的一部分)。

从由table_handle代表的Table实例中检索与给定密钥相关的行。字符串键将被UTF-8编码。 参数: 按名称列出每个参数。名称后面应跟随一个描述,用冒号后跟一个空格或新行分隔。如果描述太长而无法适合单个80个字符的行中,则使用缩进的悬挂缩进,比参数名称多2个或4个空格(与文件中的其余docstring保持一致)。如果代码不包含相应的类型注释,则描述应包括所需的类型(s) 。如果函数接受* foo(可变长度参数列表)和/或** bar(任意关键字参数),则应将它们列为* foo和** bar。

table_handle:一个open_smalltable.Table实例。 keys:一个序列,表示要获取的每个表行键。字符串键将被UTF-8编码。 require_all_keys:如果为True,仅返回为所有键设置了值的行。

返回: 一个将键映射到检索到的对应表行数据的字典。每一行表示为字符串元组。例如: {b'Serak':('Rigel VII','准备者'), b'Zim':(“Irk”,'侵入者'), b'Lrrr':(“Omicron Persei 8”,'Emperor')} 返回的键始终是字节。如果字典中缺少来自keys参数的键,则在表中找不到该行(并且require_all_keys必须为False)。 引发: IOError:访问smalltable时发生错误。

同样,这种带有换行符的_Args_变体也是允许的:“

从由table_handle代表的Table实例中检索与给定密钥相关的行。字符串键将被UTF-8编码。 参数: 按名称列出每个参数。名称后面应跟随一个描述,用冒号后跟一个空格或新行分隔。如果描述太长而无法适合单个80个字符的行中,则使用缩进的悬挂缩进,比参数名称多2个或4个空格(与文件中的其余docstring保持一致)。如果代码不包含相应的类型注释,则描述应包括所需的类型(s) 。如果函数接受* foo(可变长度参数列表)和/或** bar(任意关键字参数),则应将它们列为* foo和** bar。

table_handle: 一个open_smalltable.Table实例。 keys: 一个序列,表示要获取的每个表行键。字符串键将被UTF-8编码。

require_all_keys: 如果为True,仅返回为所有键设置了值的行。

返回: 一个将键映射到检索到的对应表行数据的字典。每一行表示为字符串元组。例如: {b'Serak':('Rigel VII','准备者'), b'Zim':(“Irk”,'侵入者'), b'Lrrr':(“Omicron Persei 8”,'Emperor')} 返回的键始终是字节。如果字典中缺少来自keys参数的键,则在表中找不到该行(并且require_all_keys必须为False)。 引发: IOError:访问smalltable时发生错误。

3.8.4 类

在类定义下方应添加一个docstring,描述类。如果您的类具有公共属性,则应在此处在“Attributes”部分中记录它们,格式与函数的“Args”部分相同。

所有类docstring都应始于一行摘要,说明类示例代表什么。这意味着`Exception的子类也应该描述异常表示的内容,而不是可能发生异常的上下文。类docstring不应重复不必要的信息,例如该类是一个类。

3.8.5 块和内联注释

评论的最后一个位置是代码的棘手部分。如果你需要在下一次代码审查时解释它,那么现在应该进行评论。在进行复杂操作之前,需要写几行注释。需要在行末添加注释以解释不明显的操作。

为了提高可读性,这些注释应该至少距离代码两个空格,使用注释字符,在文本注释之前至少有一个空格。

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

要注意标点符号,拼写和语法。读起来好的注释比读起来坏的注释更容易。

注释应与叙述文本一样易读,具有适当的大写和标点符号。在许多情况下,完整的句子比句子片段更易读。较短的注释(例如代码行末的注释)有时可以更不正式,但应始终保持风格一致。

虽然代码审查员指出你在应使用分号而不是逗号时可能感到沮丧,但源代码保持高度清晰和可读性非常重要。正确的标点符号,拼写和语法有助于实现该目标。

3.10 字符串

即使参数全都是字符串,也使用f-strings%操作符或format方法来格式化字符串。根据你的判断来决定字符串格式选项。使用" + "这个运算符进行单个连接没问题,但不要使用"+ "进行格式化。

在循环中避免使用" + "和 "+="操作符累积字符串。在某些情况下,用加法累加字符串可能会导致二次而不是线性的运行时间。虽然这种常见累加可以在 CPython 上进行优化,但这是实现细节。优化适用的条件不容易预测,可能会改变。相反,在循环结束后将每个子字符串添加到列表并使用" ''.join"连接列表,或将每个子字符串写入"io.StringIO"缓冲区。这些技术一直具有均摊线性运行时间复杂度。

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

偏爱 """ 用于多行字符串,而不是 '''。如果项目仅使用 ' 来表示常规字符串,则可以选择使用 ''' 来表示所有非文档字符串的多行字符串。文档字符串必须使用 """,无论如何。

多行字符串与程序的缩进不匹配。如果你需要避免在字符串中嵌入额外的空格,请使用连接的单行字符串或具有textwrap.dedent()的多行字符串以删除每行的初始空格。

请注意,在此处使用反斜杠并不违反明确禁止使用的显式行连续性;在这种情况下,反斜杠是在字符串字面值中转义换行符的。

3.10.1 日志记录

对于期望以 %-占位符字符串(作为其第一个参数)的日志记录函数:始终使用字符串字面值(而不是 f-string!)作为其第一个参数以及作为替代参数的模式参数。某些日志记录实现将未扩展的模式字符串作为可查询字段进行收集。它还防止浪费将没有配置记录器输出的消息所花费的时间。

3.10.2 错误信息

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

  1. 消息需要精确地匹配实际的错误条件。

  2. 插值部分必须始终明确地标识为此类部分。

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

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

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

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

  • 它们可能会消耗有限的系统资源,例如文件描述符。处理许多此类对象的代码可能会因没有及时将它们归还给系统而不必要地耗尽这些资源。

  • 持有文件可能会防止其他操作(例如移动或删除它们,或卸载文件系统)。

  • 在程序中共享的文件和套接字可能在逻辑上关闭后无意中被读取或写入。如果它们确实关闭,尝试从中读取或写入将引发异常,尽早发现问题。

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

  • 无法保证运行时实际上会何时调用 __del__ 方法。不同的 Python 实现使用不同的内存管理技术,例如延迟垃圾回收,这可能会任意和无限地增加对象的生命周期。

  • 对文件的意外引用,例如在全局变量或异常回溯中,可能会将其保存的时间长度比预期的更长。

纠结于通过终止器进行具有可观察副作用的自动清理已经被重新发现,多个语言和数十年的时间已经导致了严重问题(例如请参阅 Java 的 此文章

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

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

在无法使用基于上下文的资源管理的罕见情况下,代码文档必须清楚地解释资源的生命周期如何被管理。

3.12 TODO 注释

使用 TODO 注释表示代码是临时的、短期的解决方案或足够好但不完美的。

TODO 注释以所有大写字母 TODO 开头,后跟括号中的上下文标识符。理想情况下是bug引用,有时是用户名。例如 TODO(https://crbug.com/bug_id_number): 这样的 bug 引用更可取,因为可以跟踪 bug 并有后续评论,而个人则会随时间而移动并且可能会失去上下文。 TODO 后面是要做的事情的说明。

目的是拥有一致的 TODO 格式,可以搜索以了解如何获得更多细节。 TODO 并不意味着被提及的人会修复问题。因此,当您使用用户名创建 TODO 时,几乎总是会给出自己的用户名。

如果您的 TODO 形式为“在将来的某个时候做某事”,请确保您要么包含非常具体的日期(“修复到 2009 年 11 月”),要么包含非常具体的事件(“当所有客户端都可以处理 XML 响应时删除此代码。”) 以便将来的代码维护人员可以理解。

3.13 导入格式化

导入应该放在单独的行上;关于 typingcollections.abc 导入可以有例外。

例如:

​ ​ 错误: import os, sys

导入语句总是放在文件顶部,紧接着任何模块注释和文档字符串之后,模块全局变量和常量之前。导入应该从最通用到最不通用分组排序:

  1. Python 未来导入语句。例如:

    from future import annotations

有关更多信息,请参见上文。

  1. Python 标准库导入。例如:

    import sys

  2. 第三方模块或包导入。例如:

    import tensorflow as tf

  3. 代码库子包导入。例如:

    from otherproject.ai import mind

  4. **已过时:**与此文件属于同一顶级子包的应用程序特定导入。例如:

    from myproject.backend.hgwells import time_machine

您可能会发现旧版 Google Python 样式的代码在这样做,但现在不再需要。**鼓励新代码不要再关注这个了。**只需将应用程序特定子包导入视为其他子包导入即可。

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

​ import collections import queue import sys

3.14 语句

通常每行只有一个语句。

但是,如果整个语句都适合一行,则可以在同一行上同时测试结果和结果。特别是,无法在 try/except 中这样做,因为 tryexcept 不能都适合同一行,如果不存在 else,则仅在 if 中可以这样做。

​ 正确:

​ ​ 错误:

3.15 Getter 和 Setter

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

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

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

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

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

3.16 命名

模块名、包名、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 要避免的名称

  • 单个字符名称,但有特定允许的情况:

* 计数器或迭代器(例如ijkv等) * e 作为 try/except 语句中的异常标识符。 * f 作为 with 语句中的文件处理句柄 * 没有约束的私有类型变量(例如_T = TypeVar("_T")_P = ParamSpec("_P")

请注意不要滥用单字符命名。一般来说,描述性应该与名称的可见度范围成比例。例如,i 可以是 5 行代码块的良好名称,但在多层嵌套范围内,它很可能太模糊了。

  • 在任何包 / 模块名称中使用连字符(-)

  • __double_leading_and_trailing_underscore__ 名称(由 Python 保留)

  • 冒犯性术语

  • 不必要地包含变量类型的名称(例如:id_to_name_dict

3.16.2 命名约定

  • “Internal” 意味着在模块内部,或在类内受保护或私有。

  • 在变量和函数名前加上一个下划线(_)可以保护模块变量和函数(检查器将标记受保护成员访问)。

  • 将双下划线 (__,也称为“dunder”) 前置到实例变量或方法可使变量或方法对其类私有(使用名称重整);我们不建议使用,因为它会影响可读性和可测试性,并且不是 真正 的私有。使用单下划线。

  • 将相关类和顶层函数放在一个模块中。与 Java 不同,没有必要将每个模块限制为一个类。

  • 对于类名,使用 CapWords,但是对于模块名,使用 lower_with_under.py。虽然有一些命名为 CapWords.py 的旧模块,但现在已经不建议这样做,因为当模块恰好命名为类时会感到困惑。(import StringIO 还是 from StringIO import StringIO?)

    • 新的单元测试文件遵循符合PEP 8的下划线命名法,例如,test_<method_under_test>_<state>。为了与遵循CapWords函数名称的旧模块保持一致,方法名以test开头的方法名称中可能会出现下划线,以分隔名称的逻辑组件。一个可能的模式是test<MethodUnderTest>_<state>

3.16.3 文件命名

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

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 数学符号

对于大量涉及数学的代码,当短变量名违反样式指南时,如果它们与参考论文或算法中的已建立符号表示相匹配,则首选它们。在这样做时,将所有命名约定的来源引用到注释或docstring中,如果无法访问源,则明确记录命名约定。在公共API中使用符合PEP8的descriptive_names,这样更容易在上下文之外遇到它们。

3.17 主要

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

当使用absl时,请使用app.run

​ from absl import app ...

否则,请使用:

​ def main(): ...

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

3.18 函数长度

更喜欢简短而专注的函数。

我们认识到长函数有时是合适的,因此不会对函数长度设定硬性限制。如果函数超过约40行,请考虑不会破坏程序结构的情况下是否可以将其拆分。

即使长函数现在可以完美工作,几个月后修改它的人可能会添加新的行为。这可能导致难以发现的错误。将函数保持简短和简单使其他人更容易阅读和修改您的代码。

在使用某些代码时,您可能会发现长而复杂的函数。不要被修改现有代码所吓倒:如果使用这样的函数证明很困难,您发现错误很难调试,或者您想在几个不同的上下文中使用它的一部分,请考虑将该函数拆分成较小且更易管理的片段。

3.19 类型注释

3.19.1 一般规则

  • 熟悉PEP-484

  • 在方法中,仅注释selfcls(如果需要正确的类型信息)。例如,

    def create(cls: Type[_T]) -> _T: return cls()

  • 同样,不要感到有必要注释__init__的返回值(其中None是唯一有效的选项)。

  • 如果应该不表达任何其他变量或返回类型,则使用Any

  • 您不需要注释模块中的所有函数。

    • 至少标注您的公共API。

    • 判断保持安全性和清晰度,一方面是灵活性,另一方面是灵活性。

    • 注释容易出现类型相关错误的代码(之前的错误或复杂性)。

    • 注释难以理解的代码。

    • 注释从类型的角度变得稳定的代码。在许多情况下,您可以在成熟的代码中注释所有函数,而不会失去太多灵活性。

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允许您将闭合括号移动到新行并与开放括号对齐,但这不太可读。

以下是一个markdown文件,请将其翻译成中文,不要修改任何Markdown命令:

像上面的例子一样,最好不要打破类型。然而,有时它们太长了,不能在一行上(尽可能保持子类型不变)。

如果一个单一的名称和类型太长了,考虑使用一个类型的别名。最后的选择是在冒号后面断开并缩进4次空格。

3.19.3 前向声明

如果您需要使用尚未定义的类名(来自同一模块)——例如,如果您需要在声明该类的内部使用类名,或者如果您使用的类是稍后在代码中定义的——请使用 from __future__ import annotations 或使用一个字符串作为类名。

3.19.4 默认值

根据 PEP-008,仅针对既有类型注释又有默认值的参数,在 = 前后使用空格。

3.19.5 NoneType

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

使用明确的 X | None 而不是隐式的。PEP 484 的早期版本允许将 a: str = None 解释为 a: str | None = None,但这不再是首选行为。

3.19.6 类型别名

您可以声明复杂类型的别名。别名的名称应采用首字母大写的方式。如果该别名只在这个模块中使用,那么它应该是私有的。

请注意,版本 3.10+ 仅支持 : TypeAlias 注释。

3.19.7 忽略类型

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

pytype 有一个禁用特定错误的选项(类似于代码检查):

3.19.8 类型化变量

注释赋值

如果一个内部变量的类型很难或不可能推断,请在变量名和值之间使用冒号和类型进行注释赋值(与具有默认值的函数参数一样):

类型注释

虽然您可能还会在代码库中看到它们(它们在 Python 3.6 之前是必需的),但请不要在行末添加更多的 # type: <type name> 注释:

3.19.9 元组 vs 列表

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

3.19.10 类型变量

Python 类型系统有通用类型。一个类型变量,例如 TypeVarParamSpec,是使用它们的一种常见方法。

示例:

一个 TypeVar 可以被约束:

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

类型变量必须有一个描述性名称,除非它符合以下所有标准:

  • 不会对外部产生影响

  • 没有受限制

    正确示例: _T = TypeVar("_T") _P = ParamSpec("_P") AddableType = TypeVar("AddableType", int, float, str) AnyFunction = TypeVar("AnyFunction", bound=Callable)

    错误示例: 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

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

3.19.12 关于 Typing 的导入

用于支持静态分析和类型检查的 typingcollections.abc 模块的符号,应始终导入符号本身。这使得常用注释更简洁,并且符合全球使用的 typing 实践。您可以允许明确地从 typingcollections.abc 模块导入多个特定类。例如:

鉴于这种导入方式会向本地命名空间添加项目,因此应将 typingcollections.abc 中的名称与关键字类似处理,并且不将其定义为您的 Python 代码,无论是有类型还是没有类型。如果模块中的类型和现有名称发生冲突,则可以使用 import x as y 导入它。

尽可能使用内置类型作为注释。由于 Python 支持通过 PEP-585 使用参数化容器类型的类型注释,该机制在 Python 3.9 中引入。

注意:Apache Beam 的用户应继续从 typing 中导入参数化容器。

3.19.13 条件导入

仅在需要避免在运行时进行类型检查所需的额外导入时,才使用条件导入。不鼓励这种模式;应优先考虑替代方案,例如重构代码以允许顶层导入。

仅在 if TYPE_CHECKING: 块内放置仅用于类型注释的导入模块。

  • 需要在字符串中引用条件导入的类型,以便与在 Python 3.6 中实际运行注释表达式的前向兼容性。

  • 仅应在此处定义仅用于编写类型的实体;这包括别名。否则,它将是运行时错误,因为该模块将不会在运行时导入。

  • 该代码块应位于所有常规导入之后。

  • 类型导入列表中不应有空行。

  • 按照普通导入列表的方式对列表进行排序。

    import typing if typing.TYPE_CHECKING: import sketch def f(x: "sketch.Sketch"): ...

3.19.14 循环依赖

由于 Typing 引起的循环依赖是代码异味,因此应重构此类代码。虽然技术上可以保留循环依赖关系,但各种构建系统不会允许这样做,因为每个模块都必须依赖于其他模块。

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

3.19.15 泛型

在注释时,优先指定通用类型的类型参数; 否则将假定 generics 的参数将是"Any".

​ ​ # 错误示例: # 这将被解释为 get_names(employee_ids: Sequence[Any]) -> Mapping[Any, Any] def get_names(employee_ids: Sequence) -> Mapping: ...

如果一个泛型的最佳类型参数是 Any,则将其指定为显式的,但请记住,在许多情况下,TypeVar 可能更合适:

​ ​ # 正确示例: _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 结束语

要_保持一致_。

如果您正在编辑代码,请花几分钟时间查看周围的代码并确定其样式。如果他们在所有算术运算符周围使用空格,则您也应该使用它们。如果他们的注释有小盒子的哈希标记,请将您的注释也放在小盒子的哈希标记周围。

拥有样式指南的目的是具有共同的编码词汇,以便人们可以专注于您说的内容而不是专注于您如何表达。我们在这里提供全局样式规则,使人们了解词汇表,但本地样式也很重要。如果您添加到文件中的代码与其周围的现有代码看起来截然不同,当读者去读它时,这将打乱他们的节奏。避免这种情况。

最后更新于