Skip to Content
Python3 Python 风格规则

3 Python 风格规则

3.1 分号

不要用分号结束行,也不要用分号将两条语句放在同一行。

3.2 行长度

最大行长度为 80 个字符

80 字符限制的明确例外:

  • 长的 import 语句。
  • 注释中的 URL、路径名或长标志。
  • 不含空格的长字符串模块级常量,拆分到多行会很不方便,如 URL 或路径名。
    • Pylint 禁用注释(例如:# pylint: disable=invalid-name)。

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

相反,利用 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 字符的情况下,如果 Black Pyink  自动格式化器无法帮助将行控制在限制内,则允许行超过此最大值。鼓励作者根据上述说明在合理时手动断行。

3.3 圆括号

谨慎使用圆括号。

在元组周围使用圆括号是可以的,但不是必需的。不要在 return 语句或条件语句中使用它们,除非是用于隐式续行或表示元组。

Yes: if foo: bar() while x: x = bar() if x and y: bar() if not x: bar() # 对于单元素元组,()s 比逗号更直观。 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: # 与开始分隔符对齐。 foo = long_function_name(var_one, var_two, var_three, var_four) meal = (spam, beans) # 字典中与开始分隔符对齐。 foo = { 'long_dictionary_key': value1 + value2, ... } # 4 空格悬挂缩进;第一行不放内容。 foo = long_function_name( var_one, var_two, var_three, var_four) meal = ( spam, beans) # 4 空格悬挂缩进;第一行不放内容, # 闭合括号另起一行。 foo = long_function_name( var_one, var_two, var_three, var_four ) meal = ( spam, beans, ) # 字典中的 4 空格悬挂缩进。 foo = { 'long_dictionary_key': long_dictionary_value, ... }
No: # 第一行不应有内容。 foo = long_function_name(var_one, var_two, var_three, var_four) meal = (spam, beans) # 不允许 2 空格悬挂缩进。 foo = long_function_name( var_one, var_two, var_three, var_four) # 字典中没有悬挂缩进。 foo = { 'long_dictionary_key': long_dictionary_value, ... }

3.4.1 序列项的尾部逗号?

仅当闭合容器标记 ])} 与最后一个元素不在同一行时,以及单元素元组的情况下,才推荐使用尾部逗号(Trailing Comma)。尾部逗号的存在也被用作我们 Python 代码自动格式化器 Black Pyink  的提示,指示它在最后一个元素后的 , 存在时将容器中的项自动格式化为每行一项。

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(以支持虚拟环境)或 #!/usr/bin/python3 开始程序的主文件,遵循 PEP-394 

此行被内核用于查找 Python 解释器,但在导入模块时被 Python 忽略。它仅对打算直接执行的文件有必要。

3.8 注释和文档字符串(Docstrings)

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

3.8.1 文档字符串(Docstrings)

Python 使用*文档字符串(Docstrings)*来记录代码。文档字符串是包、模块、类或函数中的第一条语句字符串。这些字符串可以通过对象的 __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 函数和方法

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

具有以下一个或多个特征的每个函数都必须有文档字符串:

  • 属于公共 API
  • 规模不小
  • 逻辑不显而易见

文档字符串应提供足够的信息,使得无需阅读函数代码即可编写函数调用。文档字符串应描述函数的调用语法和语义,但通常不描述其实现细节,除非这些细节与函数的使用方式相关。例如,作为副作用修改其某个参数的函数应在文档字符串中说明。否则,与调用者无关的函数实现的细微但重要的细节,更适合作为代码旁边的注释而非函数的文档字符串。

文档字符串可以是描述式风格("""Fetches rows from a Bigtable.""")或祈使式风格("""Fetch rows from a Bigtable."""),但文件内的风格应保持一致。@property 数据描述符的文档字符串应使用与属性或函数参数的文档字符串相同的风格("""The Bigtable path.""",而非 """Returns the Bigtable path.""")。

函数的某些方面应在特殊部分中记录,如下所列。每个部分以一个标题行开始,该行以冒号结尾。除标题外的所有部分应保持 2 或 4 个空格的悬挂缩进(文件内保持一致)。如果函数的名称和签名足够说明性,可以用单行文档字符串恰当描述,则可以省略这些部分。

Args: : 按名称列出每个参数。描述应跟在名称后面,以冒号加一个空格或换行符分隔。如果描述太长无法放在单个 80 字符行中,使用比参数名多 2 或 4 个空格的悬挂缩进(与文件中其他文档字符串保持一致)。如果代码没有相应的类型注解,描述应包含所需的类型。如果函数接受 *foo(可变长度参数列表)和/或 **bar(任意关键字参数),应将其列为 *foo**bar

Returns:(或生成器使用 Yields: : 描述返回值的语义,包括类型注解未提供的任何类型信息。如果函数仅返回 None,则不需要此部分。如果文档字符串以 “Return”、“Returns”、“Yield” 或 “Yields” 开头(例如 """Returns row from Bigtable as a tuple of strings."""开头句子足以描述返回值,也可以省略。不要模仿旧的 ‘NumPy 风格’(示例 ),那种风格经常将元组返回值记录为多个带有各自名称的返回值(从未提及元组)。相反,应描述这样的返回值为:“Returns: A tuple (mat_a, mat_b), where mat_a is …, and …”。文档字符串中的辅助名称不必与函数体中使用的内部名称对应(因为那些不是 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 重写方法(Overridden Methods)

如果重写基类方法的方法显式使用了 @override(来自 typing_extensionstyping 模块)装饰,则不需要文档字符串,除非重写方法的行为实质性地细化了基类方法的契约,或者需要提供细节(例如记录额外的副作用),在这种情况下,重写方法需要一个至少包含这些差异的文档字符串。

from typing_extensions import override class Parent: def do_something(self): """Parent method, includes docstring.""" # 子类,方法用 override 注解。 class Child(Parent): @override def do_something(self): pass
# 子类,但没有 @override 装饰器,需要文档字符串。 class Child(Parent): def do_something(self): pass # 琐碎的文档字符串是不需要的,@override 足以表明文档可在基类中找到。 class Child(Parent): @override def do_something(self): """See base class."""

3.8.4 类

类应在类定义下方有一个描述该类的文档字符串。公共属性(不包括 properties)应在 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 块注释和行内注释

注释应放在代码中棘手的部分。如果你在下次代码审查(Code Review) 时需要解释它,你现在就应该注释它。复杂的操作在操作开始前用几行注释。不明显的操作在行末添加注释。

# 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 个空格,注释字符 # 后至少跟一个空格再写注释文本。

另一方面,永远不要描述代码。假设阅读代码的人比你更了解 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 方法来格式化字符串,即使参数全是字符串。使用你的最佳判断来决定字符串格式化选项。用 + 进行单次连接是可以的,但不要用 + 格式化。

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. 它们应允许简单的自动化处理(例如 grep)。

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 文件、套接字(Sockets)及类似有状态资源

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

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

  • 它们可能消耗有限的系统资源,如文件描述符。处理许多此类对象的代码如果在使用后没有及时将它们归还给系统,可能会不必要地耗尽这些资源。
  • 保持文件打开可能会阻止其他操作,如移动或删除文件,或卸载文件系统。
  • 在程序中共享的文件和套接字可能在逻辑上关闭后无意中被读取或写入。如果它们实际上已关闭,尝试读取或写入将引发异常,从而更早地发现问题。

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

  • 不保证运行时何时实际调用 __del__ 方法。不同的 Python 实现使用不同的内存管理技术,如延迟垃圾回收,这可能会任意且无限期地增加对象的生命周期。
  • 对文件的意外引用,例如在全局变量或异常追踪中,可能会使其保持比预期更长的时间。

依赖终结器(Finalizer)进行具有可观察副作用的自动清理,在数十年间和多种语言中被反复发现会导致重大问题(例如参见这篇关于 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("http://www.python.org/")) 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 import 语句。例如:

    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 Getter 和 Setter

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

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

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

Getter 和 setter 应遵循命名准则,如 get_foo()set_foo()

如果过去的行为允许通过属性(Property)访问,不要将新的 getter/setter 函数绑定到属性。仍然尝试通过旧方式访问变量的代码应当明显地报错,以便让开发者意识到复杂性的变化。

3.16 命名

module_namepackage_nameClassNamemethod_nameExceptionNamefunction_nameGLOBAL_CONSTANT_NAMEglobal_var_nameinstance_var_namefunction_parameter_namelocal_var_namequery_proper_noun_for_thingsend_acronym_via_https

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

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

始终使用 .py 文件扩展名。不要使用连字符。

3.16.1 应避免的名称

  • 单字符名称,除了以下特别允许的情况:

    • 计数器或迭代器(例如 ijkv 等)
    • try/except 语句中的异常标识符 e
    • with 语句中的文件句柄 f
    • 没有约束的私有类型变量(例如 _T = TypeVar("_T")_P = ParamSpec("_P")
    • 与参考论文或算法中已建立的符号匹配的名称(参见数学符号

    请注意不要滥用单字符命名。一般来说,描述性应与名称的可见范围成正比。例如,i 对于 5 行代码块可能是一个好名字,但在多个嵌套作用域中,它可能太模糊了。

  • 任何包/模块名中的连字符(-

  • __double_leading_and_trailing_underscore__ 名称(Python 保留的)

  • 冒犯性术语

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

3.16.2 命名约定

  • “内部(Internal)“指模块内部的,或类中受保护的或私有的。

  • 在前面加单下划线(_)对保护模块变量和函数有一定支持(lint 工具会标记受保护成员访问)。注意,单元测试可以访问被测模块中的受保护常量。

  • 在实例变量或方法前加双下划线(__,又称 “dunder”)实际上会使变量或方法对其类私有(使用名称修饰);我们不鼓励使用它,因为它影响可读性和可测试性,而且并非真正私有。优先使用单下划线。

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

  • 类名使用大驼峰命名(CapWords),但模块名使用小写加下划线(lower_with_under.py)。虽然有一些旧模块以大驼峰命名(CapWords.py),但现在不鼓励这样做,因为当模块碰巧以类命名时会令人困惑。(“等等——我写的是 import StringIO 还是 from StringIO import StringIO?”)

  • 新的单元测试文件遵循 PEP 8 兼容的小写加下划线方法名,例如 test_<method_under_test>_<state>。为了与遵循大驼峰函数名的遗留模块保持一致(*),以 test 开头的方法名中可以出现下划线来分隔名称的逻辑组成部分。一种可能的模式是 test<MethodUnderTest>_<state>

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 数学符号(Mathematical Notation)

对于数学密集型代码,当短变量名与参考论文或算法中已建立的符号匹配时,优先使用这些短变量名(即使它们在其他情况下违反风格指南)。

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

  1. 在注释或文档字符串中引用所有命名约定的来源,最好附上学术资源本身的超链接。如果来源不可访问,请清楚地记录命名约定。
  2. 对于公共 API,优先使用符合 PEP 8 的 descriptive_names,因为它们更可能在上下文之外被遇到。
  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()

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

3.18 函数长度

优先选择小而专注的函数。

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

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

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

3.19 类型注解(Type Annotations)

3.19.1 通用规则

  • 熟悉类型提示(Type Hints) 

  • 注解 selfcls 通常不是必需的。如果需要为了正确的类型信息可以使用 Self,例如

    from typing import Self class BaseClass: @classmethod def create(cls) -> Self: ... def difference(self, other: Self) -> float: ...
  • 同样,不必非要注解 __init__ 的返回值(None 是唯一有效的选项)。

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

  • 你不需要注解模块中的所有函数。

    • 至少注解你的公共 API。
    • 在安全性和清晰性与灵活性之间做出良好的平衡判断。
    • 注解容易出现类型相关错误的代码(之前的 bug 或复杂性)。
    • 注解难以理解的代码。
    • 从类型角度来看,当代码变得稳定时注解它。在许多情况下,你可以注解成熟代码中的所有函数而不会失去太多灵活性。

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 前向声明(Forward Declarations)

如果你需要使用尚未定义的类名(来自同一模块)——例如,如果你需要在类的声明内部使用该类名,或者如果你使用了代码中后面定义的类——要么使用 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 类型别名(Type Aliases)

你可以声明复杂类型的别名。别名的名称应使用大驼峰命名(CapWords)。如果别名仅在本模块中使用,应以 _ 开头使其私有。

注意 : 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 类型变量标注

带注解的赋值(Annotated Assignments) : 如果内部变量的类型难以或无法推断,使用带注解的赋值来指定其类型——在变量名和值之间使用冒号和类型(与具有默认值的函数参数相同的做法):

```python a: Foo = SomeUndecoratedFunction() ```

类型注释(Type Comments) : 虽然你可能会在代码库中看到它们保留着(在 Python 3.6 之前它们是必需的),但不要再添加任何在行末使用 # type: <type name> 注释的新用法:

```python a = SomeUndecoratedFunction() # type: Foo ```

3.19.9 元组(Tuples) vs 列表(Lists)

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

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

3.19.10 类型变量(Type Variables)

Python 类型系统有泛型(Generics) 。类型变量(如 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 条件导入(Conditional Imports)

仅在需要避免因类型检查而引入额外运行时导入的特殊情况下使用条件导入。不鼓励此模式;应优先选择重构代码以允许顶层导入等替代方案。

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

  • 条件导入的类型需要作为字符串引用,以便向前兼容 Python 3.6,其中注解表达式实际上会被求值。
  • 这里只应定义仅用于类型标注的实体;这包括别名。否则将产生运行时错误,因为模块在运行时不会被导入。
  • 该块应紧接在所有正常导入之后。
  • 类型导入列表中不应有空行。
  • 此列表的排序方式应与常规导入列表相同。
import typing if typing.TYPE_CHECKING: import sketch def f(x: "sketch.Sketch"): ...

3.19.14 循环依赖(Circular Dependencies)

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

将创建循环依赖导入的模块替换为 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 泛型(Generics)

注解时,优先为泛型 类型在参数列表中指定类型参数;否则,泛型的参数将被假定为 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."""
Last updated on