英文原文地址:https://peps.python.org/pep-0008/

简介

本文档给出了 Python 主发行版中标准库 Python 代码的编码规范。有关 Python 的 C 语言实现中 C 代码的风格指南,请参阅相关的信息性 PEP 文档。

本文档和 PEP 257(文档字符串约定)改编自 Guido 最初的 Python 风格指南文章,并融入了 Barry 风格指南中的一些内容。

随着新约定的确定以及语言本身的变化使过去的约定过时,本风格指南也在不断演进。

许多项目都有自己的编码风格指南。如有冲突,特定项目的指南优先适用于该项目。

盲目一致性是狭隘思想的表现

Guido 的一个重要观点是,代码的阅读频率远高于编写频率。这里提供的指南旨在提高代码的可读性,并使各种 Python 代码的风格保持一致。正如 PEP 20 所说:“可读性很重要”。

风格指南关乎一致性。与本风格指南保持一致很重要,项目内部的一致性更为重要,而一个模块或函数内部的一致性则是最重要的。

然而,要知道何时可以不一致 —— 有时风格指南的建议并不适用。如有疑问,运用自己的最佳判断。参考其他示例,判断哪种看起来最佳。不要犹豫,有问题就问!

特别要注意:不要仅仅为了遵守本 PEP 而破坏向后兼容性!

还有一些合理的理由可以忽略特定的指南:

  • 应用该指南会使代码的可读性降低,即使对于习惯阅读遵循本 PEP 代码的人也是如此。
  • 为了与周围同样违反该指南的代码保持一致(可能是出于历史原因)—— 尽管这也是一个清理他人代码混乱的机会(以真正的极限编程风格)。
  • 因为相关代码早于该指南的引入,并且没有其他理由对其进行修改。
  • 当代码需要与不支持风格指南所推荐特性的旧版本 Python 保持兼容时。

代码布局

缩进

每个缩进级别使用 4 个空格。

续行应使用 Python 在括号、方括号和花括号内的隐式行连接功能,将换行的元素垂直对齐,或者使用悬挂缩进。使用悬挂缩进时应注意:第一行不应有参数,并且应进一步缩进以明确表示这是一个续行:

# 正确:
# 与起始分隔符对齐。
foo = long_function_name(var_one, var_two,
                         var_three, var_four)
# 添加4个空格(额外一级缩进)以区分参数与其他部分。
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)
# 悬挂缩进应增加一级。
foo = long_function_name(
    var_one, var_two,
    var_three, var_four)
# 错误:
# 不使用垂直对齐时,第一行不允许有参数。
foo = long_function_name(var_one, var_two,
    var_three, var_four)
# 需要进一步缩进,因为缩进无法区分。
def long_function_name(
    var_one, var_two, var_three,
    var_four):
    print(var_one)

4 个空格的规则对于续行是可选的。

# 可选:
# 悬挂缩进可以缩进为4个空格以外的其他空格数。
foo = long_function_name(
  var_one, var_two,
  var_three, var_four)

if语句的条件部分很长,需要跨多行书写时,值得注意的是,两个字符的关键字(即if)加上一个空格,再加上一个左括号,会为后续多行条件的行创建一个自然的 4 个空格缩进。这可能会与if语句内部嵌套的缩进代码块产生视觉冲突,因为代码块自然也会缩进 4 个空格。对于如何(或是否)在视觉上进一步区分这些条件行与if语句内部的嵌套代码块,本 PEP 没有明确立场。在这种情况下,可接受的选项包括但不限于:

# 不额外缩进。
if (this_is_one_thing and
    that_is_another_thing):
    do_something()
# 添加注释,这将在支持语法高亮的编辑器中提供一些区分。
if (this_is_one_thing and
    that_is_another_thing):
    # 由于两个条件都为真,我们可以进行frobnicate操作。
    do_something()
# 在条件续行上添加一些额外缩进。
if (this_is_one_thing
        and that_is_another_thing):
    do_something()

(另请参阅下面关于在二元运算符之前还是之后换行的讨论。)

多行结构的右括号 / 右方括号 / 右圆括号可以与列表最后一行的第一个非空白字符对齐,例如:

my_list = [
    1, 2, 3,
    4, 5, 6,
    ]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
    )

也可以与开始多行结构的行的第一个字符对齐,例如:

my_list = [
    1, 2, 3,
    4, 5, 6,
]
result = some_function_that_takes_arguments(
    'a', 'b', 'c',
    'd', 'e', 'f',
)

制表符还是空格?

空格是首选的缩进方法。

仅在与已经使用制表符缩进的代码保持一致时,才应使用制表符。

Python 不允许在缩进中混合使用制表符和空格。

最大行长

所有行的长度限制为最多 79 个字符。

对于结构限制较少的长文本块(文档字符串或注释),行长应限制为 72 个字符。

限制所需的编辑器窗口宽度,可以使多个文件并排打开,并且在使用将两个版本显示在相邻列中的代码审查工具时效果很好。

大多数工具的默认换行设置会破坏代码的视觉结构,使其更难理解。选择这些限制是为了避免在窗口宽度设置为 80 的编辑器中换行,即使工具在换行时会在最后一列放置一个标记符号。一些基于 Web 的工具可能根本不提供动态换行功能。

有些团队强烈倾向于更长的行长。对于完全或主要由能够就该问题达成一致的团队维护的代码,可以将行长限制增加到 99 个字符,但前提是注释和文档字符串仍保持 72 个字符的换行限制。

Python 标准库较为保守,要求将行限制为 79 个字符(文档字符串 / 注释为 72 个字符)。

包装长行的首选方法是使用 Python 在括号、方括号和花括号内的隐式行延续功能。可以通过将表达式包装在括号中来将长行拆分为多行。应优先使用这种方法,而不是使用反斜杠进行行延续。

反斜杠在某些情况下仍然适用。例如,在 Python 3.10 之前,长的多个with语句无法使用隐式延续,因此在这种情况下使用反斜杠是可以接受的:

with open('/path/to/some/file/you/want/to/read') as file_1, \
     open('/path/to/some/file/being/written', 'w') as file_2:
    file_2.write(file_1.read())

(有关此类多行with语句缩进的进一步思考,请参阅前面关于多行if语句的讨论。)

另一种情况是assert语句。

确保对续行进行适当的缩进。

应在二元运算符之前还是之后换行?

几十年来,推荐的风格是在二元运算符之后换行。但这可能会在两个方面损害可读性:运算符往往会分散在屏幕的不同列中,并且每个运算符都与其操作数分离并移动到上一行。在这种情况下,眼睛需要额外的努力才能分辨出哪些项是相加的,哪些是相减的:

# 错误:
# 运算符与操作数相距较远
income = (gross_wages +
          taxable_interest +
          (dividends - qualified_dividends) -
          ira_deduction -
          student_loan_interest)

为了解决这个可读性问题,数学家及其出版商遵循相反的约定。Donald Knuth 在他的《计算机与排版》系列中解释了传统规则:“尽管段落内的公式总是在二元运算和关系之后换行,但显示的公式总是在二元运算之前换行”。

遵循数学传统通常会使代码更具可读性:

# 正确:
# 易于将运算符与操作数匹配
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

在 Python 代码中,在二元运算符之前或之后换行都是允许的,只要在局部保持一致即可。对于新代码,建议采用 Knuth 的风格。

空行

在顶级函数和类定义的周围使用两个空行。

类内部的方法定义周围使用一个空行。

可以(谨慎地)使用额外的空行来分隔相关函数组。在一组相关的单行代码(例如一组虚拟实现)之间可以省略空行。

在函数中谨慎地使用空行,以表示逻辑部分。

Python 接受控制 - L(即 ^L)换页字符作为空白;许多工具将这些字符视为页面分隔符,因此你可以使用它们来分隔文件中相关部分的页面。请注意,一些编辑器和基于 Web 的代码查看器可能无法识别控制 - L 作为换页符,并会显示其他符号来代替。

源文件编码

核心 Python 发行版中的代码应始终使用 UTF-8 编码,并且不应有编码声明。

在标准库中,非 UTF-8 编码仅应用于测试目的。应谨慎使用非 ASCII 字符,最好仅用于表示地点和人名。如果将非 ASCII 字符用作数据,应避免使用像 z̯̯͡a̧͎̺l̡͓̫g̹̲o̡̼̘这样繁杂的 Unicode 字符和字节顺序标记。

Python 标准库中的所有标识符必须使用仅包含 ASCII 字符的标识符,并且在可行的情况下应使用英文单词(在许多情况下,会使用缩写和技术术语,这些不一定是英文单词)。

鼓励面向全球受众的开源项目采用类似的策略。

导入

导入通常应在单独的行上:

# 正确:
import os
import sys
# 错误:
import sys, os

不过,这样写是可以的:

# 正确:
from subprocess import Popen, PIPE

导入始终放在文件的顶部,紧跟在任何模块注释和文档字符串之后,在模块全局变量和常量之前。

导入应按以下顺序分组:

  1. 标准库导入。
  2. 相关的第三方库导入。
  3. 本地应用程序 / 库特定导入。

每组导入之间应留一个空行。

建议使用绝对导入,因为它们通常更具可读性,并且在导入系统配置不正确(例如,包内的某个目录最终出现在sys.path中)时,表现往往更好(或者至少会给出更好的错误消息):

import mypkg.sibling
from mypkg import sibling
from mypkg.sibling import example

然而,显式相对导入是绝对导入的可接受替代方案,特别是在处理复杂的包布局时,使用绝对导入会显得过于冗长:

from . import sibling
from .sibling import example

标准库代码应避免复杂的包布局,始终使用绝对导入。

从包含类的模块中导入类时,通常这样写是可以的:

from myclass import MyClass
from foo.bar.yourclass import YourClass

如果这种写法导致局部名称冲突,则应显式地进行导入:

import myclass
import foo.bar.yourclass

并使用myclass.MyClassfoo.bar.yourclass.YourClass

应避免使用通配符导入(from <module> import*),因为它们会使命名空间中存在哪些名称变得不清晰,这会让读者和许多自动化工具感到困惑。有一个可以使用通配符导入的合理用例,即作为公共 API 的一部分重新发布内部接口(例如,用可选加速器模块的定义覆盖接口的纯 Python 实现,并且事先不知道哪些定义会被覆盖)。

以这种方式重新发布名称时,以下关于公共和内部接口的指南仍然适用。

模块级双下划线名称

模块级的 “双下划线” 名称(即带有两个前导和两个尾随下划线的名称),如__all____author____version__等,应放在模块文档字符串之后,但在除from __future__导入之外的任何导入语句之前。Python 规定,未来导入必须出现在模块中除文档字符串之外的任何其他代码之前:

"""这是示例模块。
这个模块用于执行某些操作。
"""
from __future__ import barry_as_FLUFL
__all__ = ['a', 'b', 'c']
__version__ = '0.1'
__author__ = 'Cardinal Biggles'
import os
import sys

字符串引号

在 Python 中,单引号字符串和双引号字符串是相同的。本 PEP 对此不做推荐。选择一种规则并坚持使用。但是,当字符串包含单引号或双引号字符时,应使用另一种引号,以避免在字符串中使用反斜杠,这样可以提高可读性。

对于三引号字符串,始终使用双引号,以与 PEP 257 中的文档字符串约定保持一致。

表达式和语句中的空白

特别注意事项

在以下情况下避免多余的空白:

  • 紧接在括号、方括号或花括号内部:
# 正确:
spam(ham[1], {eggs: 2})
# 错误:
spam( ham[ 1 ], { eggs: 2 } )
  • 在尾随逗号和紧随其后的右括号之间:
# 正确:
foo = (0,)
# 错误:
bar = (0, )
  • 紧接在逗号、分号或冒号之前:
# 正确:
if x == 4: print(x, y); x, y = y, x
# 错误:
if x == 4 : print(x , y) ; x , y = y , x

不过,在切片中,冒号的作用类似于二元运算符,其两侧的空白数量应相等(将其视为优先级最低的运算符)。在扩展切片中,两个冒号的两侧都必须应用相同数量的空白。例外情况是:当切片参数被省略时,空白也应省略:

# 正确:
ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]
ham[lower:upper], ham[lower:upper:], ham[lower::step]
ham[lower+offset : upper+offset]
ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]
ham[lower + offset : upper + offset]
# 错误:
ham[lower + offset:upper + offset]
ham[1: 9], ham[1 :9], ham[1:9 :3]
ham[lower : : step]
ham[ : upper]
  • 紧接在函数调用参数列表的左括号之前:
# 正确:
spam(1)
# 错误:
spam (1)
  • 紧接在开始索引或切片的左括号之前:
# 正确:
dct['key'] = lst[index]
# 错误:
dct ['key'] = lst [index]
  • 在赋值(或其他)运算符周围使用多个空格来与另一个运算符对齐:
# 正确:
x = 1
y = 2
long_variable = 3
# 错误:
x             = 1
y             = 2
long_variable = 3

其他建议

避免在任何地方出现尾随空白。由于它通常不可见,可能会造成混淆:例如,反斜杠后面跟着一个空格和一个换行符,这并不被视为行延续标记。一些编辑器不会保留它,而且许多项目(如 CPython 本身)都有预提交钩子来拒绝它。

始终在这些二元运算符的两侧各使用一个空格:赋值(=)、增强赋值(+=-=等)、比较(==<>!=<><=>=innot inisis not)、布尔运算(andornot)。

如果使用了不同优先级的运算符,可以考虑在优先级最低的运算符周围添加空白。自行判断,但绝不要使用超过一个空格,并且在二元运算符的两侧始终保持相同数量的空白:

# 正确:
i = i + 1
submitted += 1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
# 错误:
i=i+1
submitted +=1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)

函数注释应遵循冒号的常规规则,如果存在->箭头,其周围始终要有空格。(有关函数注释的更多信息,请参阅下面的 “函数注释” 部分。):

# 正确:
def munge(input: AnyStr): ...
def munge() -> PosInt: ...
# 错误:
def munge(input:AnyStr): ...
def munge()->PosInt: ...

当 “=” 用于表示关键字参数,或者用于表示未添加注释的函数参数的默认值时,不要在其周围使用空格:

# Correct:
def complex(real, imag=0.0):
    return magic(r=real, i=imag)
# Wrong:
def complex(real, imag = 0.0):
    return magic(r = real, i = imag)

然而,当将参数注解与默认值结合使用时,确实要在 “=” 符号周围使用空格:

# Correct:
def munge(sep: AnyStr = None): ...
def munge(input: AnyStr, sep: AnyStr = None, limit=1000): ...
# Wrong:
def munge(input: AnyStr=None): ...
def munge(input: AnyStr, limit = 1000): ...

复合语句(同一行上的多个语句)一般不鼓励使用

# 正确:
if foo == 'blah':
    do_blah_thing()
do_one()
do_two()
do_three()

不要这样写

# 错误:
if foo == 'blah': do_blah_thing()
do_one(); do_two(); do_three()

虽然有时将 if/for/while 语句及其简短的代码块写在同一行是可行的,但对于多子句语句,绝不要这样做。同时,也要避免将这样的长行折叠! 不要这样写

# 错误:
if foo == 'blah': do_blah_thing()
for x in lst: total += x
while t < 10: t = delay()

绝对不要这样写

# 错误:
if foo == 'blah': do_blah_thing()
else: do_non_blah_thing()

try: something()
finally: cleanup()

do_one(); do_two(); do_three(long, argument,
                             list, like, this)

if foo == 'blah': one(); two(); three()

何时使用尾随逗号

尾随逗号通常是可选的,但在创建单元素元组时是必需的。为了清晰起见,建议用(从技术上讲是冗余的)括号将其括起来:

# 正确:
FILES = ('setup.cfg',)
# 错误:
FILES = 'setup.cfg',

当尾随逗号是冗余的时候,如果使用版本控制系统,并且预计值列表、参数列表或导入项列表会随着时间的推移而扩展,那么尾随逗号通常是有帮助的。其模式是将每个值(等)单独放在一行上,始终添加一个尾随逗号,并在下行添加右括号 / 方括号 / 花括号。但是,除了上述单元素元组的情况外,在与结束分隔符同一行上有一个尾随逗号是没有意义的:

# 正确:
FILES = [
    'setup.cfg',
    'tox.ini',
    ]
initialize(FILES,
           error=True,
           )
# 错误:
FILES = ['setup.cfg', 'tox.ini',]
initialize(FILES, error=True,)

注释

与代码相矛盾的注释比没有注释更糟糕。当代码发生变化时,始终要优先保持注释是最新的!

注释应该是完整的句子。除非第一个单词是一个以小写字母开头的标识符(永远不要改变标识符的大小写!),否则第一个单词应该大写。

块注释通常由一个或多个由完整句子组成的段落构成,每个句子都以句号结尾。

在多句注释中,除了最后一个句子外,在句子结尾的句号后应该使用一到两个空格。

确保你的注释对于使用你所编写语言的其他人来说是清晰易懂的。

非英语国家的 Python 程序员:请用英语写注释,除非你百分百确定这段代码永远不会被不会说你所用语言的人阅读。

块注释

块注释通常适用于它们后面的部分(或全部)代码,并且缩进与该代码相同。块注释的每一行都以一个 # 和一个空格开头(除非它是注释内缩进的文本)。

块注释内的段落由只包含一个 # 的行分隔。

行内注释

谨慎使用行内注释。

行内注释是与语句在同一行的注释。行内注释与语句之间至少应该用两个空格隔开。它们应该以一个 # 和一个空格开头。

如果行内注释陈述的是显而易见的内容,那么它们是不必要的,实际上还会分散注意力。不要这样做:

x = x + 1                 # 增加x的值

但有时,这样做是有用的:

x = x + 1                 # 补偿边界值

文档字符串

编写良好文档字符串(也称为 “docstrings”)的约定在 PEP 257 中有详细说明。

为所有公共模块、函数、类和方法编写文档字符串。对于非公共方法,文档字符串不是必需的,但你应该有一个注释来描述该方法的功能。这个注释应该出现在 def 行之后。

PEP 257 描述了良好的文档字符串约定。请注意,最重要的是,结束多行文档字符串的 """ 应该单独在一行上:

"""返回一个foobang。

可选的plotz参数表示首先要对bizbaz进行frobnicate操作。
"""

对于单行文档字符串,请将结束的 """ 保持在同一行上:

"""返回一只死去的鹦鹉。"""

命名约定

Python 库的命名约定有点混乱,所以我们永远无法做到完全一致 —— 尽管如此,以下是目前推荐的命名标准。新的模块和包(包括第三方框架)应该按照这些标准编写,但如果现有库有不同的风格,则优先保持内部一致性。

首要原则

作为 API 的公共部分对用户可见的名称,应该遵循反映用法而非实现的约定。

描述性:命名风格

有很多不同的命名风格。能够识别正在使用的命名风格是很有帮助的,这与它们的用途无关。

通常可以区分以下命名风格:

  • b(单个小写字母)
  • B(单个大写字母)
  • lowercase(小写字母)
  • lower_case_with_underscores(小写字母加下划线)
  • UPPERCASE(大写字母)
  • UPPER_CASE_WITH_UNDERSCORES(大写字母加下划线)
  • CapitalizedWords(或CapWords,或CamelCase—— 之所以这样命名,是因为其字母看起来凹凸不平)。这有时也被称为StudlyCaps注意:在CapWords中使用缩写词时,要将缩写词的所有字母都大写。因此,HTTPServerErrorHttpServerError更好。
  • mixedCase(与CapitalizedWords的区别在于首字母为小写!)
  • Capitalized_Words_With_Underscores(很难看!)

还有一种风格是使用一个简短的唯一前缀来将相关名称组合在一起。这种风格在 Python 中不太常用,但为了完整起见在此提及。例如,os.stat()函数返回一个元组,其元素传统上具有诸如st_modest_sizest_mtime等名称。(这样做是为了强调与 POSIX 系统调用结构的字段的对应关系,这对熟悉该结构的程序员有帮助。)

X11 库对其所有公共函数都使用前导X。在 Python 中,这种风格通常被认为是不必要的,因为属性和方法名称都以对象为前缀,函数名称以模块名称为前缀。

此外,还认可以下使用前导或尾随下划线的特殊形式(这些形式通常可以与任何大小写约定结合使用):

  • _single_leading_underscore:弱 “内部使用” 指示符。例如,from M import *不会导入名称以下划线开头的对象。
  • single_trailing_underscore_:按照约定用于避免与 Python 关键字冲突,例如:tkinter.Toplevel(master, class_='ClassName')
  • __double_leading_underscore:在命名类属性时,会调用名称改写机制(在类FooBar中,__boo会变成_FooBar__boo;详见下文)。
  • __double_leading_and_trailing_underscore__:存在于用户控制的命名空间中的 “魔术” 对象或属性。例如,__init____import____file__。永远不要发明这样的名称;仅按文档说明使用它们。

规范性:命名约定

  1. 应避免使用的名称:永远不要使用字符l(小写字母 el)、O(大写字母 oh)或I(大写字母 eye)作为单个字符的变量名。在某些字体中,这些字符与数字 1 和 0 难以区分。如果想用l,请改用L
  2. ASCII 兼容性:标准库中使用的标识符必须与 ASCII 兼容,如 PEP 3131 的策略部分所述。
  3. 包和模块名称:模块应该有简短的全小写名称。如果能提高可读性,模块名中可以使用下划线。Python 包也应该有简短的全小写名称,不过不鼓励使用下划线。当用 C 或 C++ 编写的扩展模块有一个配套的 Python 模块,用于提供更高级(例如更面向对象)的接口时,C/C++ 模块有一个前导下划线(例如_socket)。
  4. 类名:类名通常应使用CapWords约定。在接口有文档记录且主要用作可调用对象的情况下,可以使用函数的命名约定代替。注意,内置名称有单独的约定:大多数内置名称是单个单词(或两个单词连在一起),只有异常名称和内置常量使用CapWords约定。
  5. 类型变量名称:PEP 484 中引入的类型变量名称通常应使用CapWords,并倾向于使用短名称:TAnyStrNum。建议分别给用于声明协变或逆变行为的变量添加后缀_co_contra:  
    from typing import TypeVar
    
    VT_co = TypeVar('VT_co', covariant=True)
    KT_contra = TypeVar('KT_contra', contravariant=True)

  6. 异常名称:因为异常应该是类,所以这里适用类命名约定。不过,异常名称(如果该异常确实是一个错误)应该使用后缀 “Error”。
  7. 全局变量名称:(希望这些变量仅在一个模块内部使用。)其约定与函数的约定大致相同。为通过from M import *使用而设计的模块,应该使用__all__机制来防止导出全局变量,或者使用较旧的约定,即在这些全局变量前加一个下划线(这样做可能是为了表明这些全局变量 “非模块公共”)。
  8. 函数和变量名称:函数名称应该是小写的,必要时用下划线分隔单词以提高可读性。变量名称遵循与函数名称相同的约定。只有在已经普遍采用mixedCase风格的上下文中(例如threading.py),才允许使用这种风格,以保持向后兼容性。
  9. 函数和方法参数:实例方法的第一个参数始终使用self。类方法的第一个参数始终使用cls。如果函数参数的名称与保留关键字冲突,通常最好追加一个尾随下划线,而不是使用缩写或错误拼写。因此,class_clss更好。(或许更好的做法是通过使用同义词来避免这种冲突。)
  10. 方法名称和实例变量:使用函数命名规则:小写,必要时用下划线分隔单词以提高可读性。仅对非公共方法和实例变量使用一个前导下划线。为避免与子类发生命名冲突,使用两个前导下划线来调用 Python 的名称改写规则。Python 会将这些名称与类名进行改写:如果类Foo有一个名为__a的属性,就不能通过Foo.__a访问它。(执着的用户仍然可以通过调用Foo._Foo__a来访问它。)通常,双前导下划线应该只用于避免与为子类设计的类中的属性发生命名冲突。注意:关于__names的使用存在一些争议(详见下文)。
  11. 常量:常量通常在模块级别定义,全部用大写字母书写,并用下划线分隔单词。例如MAX_OVERFLOWTOTAL
  12. 为继承而设计  

        始终要决定类的方法和实例变量(统称为 “属性”)应该是公共的还是非公共的。如果拿不准,选择非公共的;因为以后将其变为公共的比将公共属性变为非公共的要容易。公共属性是你期望类的无关客户端使用的属性,并且你承诺避免进行向后不兼容的更改。非公共属性是不打算供第三方使用的属性;你不保证非公共属性不会更改甚至被删除。

        我们在这里不使用 “私有” 这个术语,因为在 Python 中没有真正的私有属性(除非做大量通常不必要的工作)。

        另一类属性是属于 “子类 API” 的属性(在其他语言中通常称为 “受保护的”)。有些类是为了被继承而设计的,目的是扩展或修改类的行为。在设计这样的类时,要仔细明确地决定哪些属性是公共的,哪些是子类 API 的一部分,哪些实际上仅由基类使用。

考虑到这一点,以下是 Python 的指导原则:

  • 公共属性不应有前导下划线。
  • 如果你的公共属性名称与保留关键字冲突,在属性名称后追加一个尾随下划线。这比使用缩写或错误拼写更好。(不过,尽管有此规则,对于任何已知为类的变量或参数,尤其是类方法的第一个参数,cls是首选拼写。)注意 1:请参阅上面关于类方法参数名称的建议。
  • 对于简单的公共数据属性,最好只暴露属性名称,而不使用复杂的访问器 / 修改器方法。请记住,如果简单的数据属性需要增加功能行为,Python 提供了一条轻松的未来增强路径。在这种情况下,可以使用属性(properties)将功能实现隐藏在简单的数据属性访问语法之后。注意 1:尽量使功能行为无副作用,不过诸如缓存之类的副作用通常是可以接受的。注意 2:避免对计算成本高的操作使用属性;属性表示法会使调用者认为访问是(相对)廉价的。
  • 如果你的类旨在被子类化,并且你有不希望子类使用的属性,可以考虑用双前导下划线且无尾随下划线来命名它们。这会调用 Python 的名称改写算法,将类名融入属性名称中。这有助于避免子类无意中包含相同名称的属性时发生属性名称冲突。注意 1:请注意,改写后的名称中仅使用简单的类名,因此如果子类选择了相同的类名和属性名,仍然可能会发生名称冲突。注意 2:名称改写可能会使某些用途(如调试和__getattr__())变得不太方便。然而,名称改写算法有详细记录,并且手动执行也很容易。注意 3:并非每个人都喜欢名称改写。尽量在避免意外名称冲突的需求与高级调用者的潜在使用之间取得平衡。

公共接口和内部接口

任何向后兼容性保证仅适用于公共接口。因此,用户能够清楚地区分公共接口和内部接口非常重要。

有文档记录的接口被视为公共接口,除非文档明确声明它们是临时接口或内部接口,不受通常的向后兼容性保证约束。所有无文档记录的接口都应被视为内部接口。

为了更好地支持内省,模块应该使用__all__属性显式声明其公共 API 中的名称。将__all__设置为空列表表示该模块没有公共 API。

即使__all__设置正确,内部接口(包、模块、类、函数、属性或其他名称)仍应以单个前导下划线为前缀。

如果任何包含命名空间(包、模块或类)被视为内部的,那么该接口也被视为内部接口。

导入的名称应始终被视为实现细节。其他模块不应依赖间接访问此类导入的名称,除非它们是包含模块 API 的明确记录的一部分,例如os.path或从子模块公开功能的包的__init__模块。

编程建议

  • 代码的编写方式不应使 Python 的其他实现(如 PyPy、Jython、IronPython、Cython、Psyco 等)处于劣势。

        例如,不要依赖 CPython 对 a += ba = a + b 形式的语句所采用的高效就地字符串拼接实现。这种优化即便在 CPython 里也很不可靠(它仅对某些类型有效),而且在不使用引用计数的实现中根本不存在。在库中对性能敏感的部分,应该使用 ''.join() 形式。这将确保在各种实现中,字符串拼接都能以线性时间完成。

  • None 等单例进行比较时,应始终使用 isis not,绝不要使用相等运算符。

        另外,当你真正想表达的是 if x is not None 时,要注意避免写成 if x。例如,当测试一个默认值为 None 的变量或参数是否被设置为其他值时,这个其他值可能是某种在布尔上下文中会被视为假值的类型(比如容器类型)!

  • 使用 is not 运算符,而不是 not ... is。虽然这两个表达式在功能上是相同的,但前者更易读,因此更受推荐:
# 正确示例
if foo is not None:
# 错误示例
if not foo is None:
  • 在使用富比较实现排序操作时,最好实现全部六个比较方法(__eq____ne____lt____le____gt____ge__),而不是依赖其他代码只调用某一种特定的比较。

       为了尽量减少工作量,functools.total_ordering() 装饰器提供了一种工具来生成缺失的比较方法。

        PEP 207 指出,Python 假定了自反性规则。因此,解释器可能会将 y > xx < y 互换,将 y >= xx <= y 互换,也可能会交换 x == yx != y 的参数。sort()min() 操作保证会使用 < 运算符,而 max() 函数会使用 > 运算符。不过,最好还是实现全部六个方法,以免在其他上下文中产生混淆。

  • 始终使用 def 语句来定义函数,而不是使用赋值语句将 lambda 表达式直接绑定到标识符:
# 正确示例
def f(x): return 2*x
# 错误示例
f = lambda x: 2*x

        第一种形式意味着生成的函数对象的名称明确是 f,而不是通用的 <lambda>。这在回溯追踪和字符串表示方面通常更有用。使用赋值语句会消除 lambda 表达式相对于显式 def 语句的唯一优势(即它可以嵌入到更大的表达式中)。

  • 自定义异常时,应从 Exception 类派生,而不是从 BaseException 类派生。直接从 BaseException 派生仅适用于那些捕获它们几乎总是错误做法的异常。

        设计异常层次结构时,应基于捕获异常的代码可能需要的区分条件,而不是异常抛出的位置。目标是以编程方式回答 “哪里出问题了?”,而不仅仅是说明 “出现了问题”(关于内置异常层次结构吸取此教训的示例,请参阅 PEP 3151)。

        类的命名约定同样适用于异常类。不过,如果异常代表错误,应在异常类名后添加 “Error” 后缀。用于非局部流程控制或其他信号形式的非错误异常无需特殊后缀。

  • 要恰当地使用异常链。使用 raise X from Y 来明确替换异常,同时保留原始的回溯信息。

        当有意替换内部异常(使用 raise X from None)时,要确保将相关细节转移到新异常中(例如,将 KeyError 转换为 AttributeError 时保留属性名,或者将原始异常的文本嵌入到新异常消息中)。

  • 捕获异常时,尽可能指定具体的异常,而不是使用无具体异常的 except 子句:
try:
    import platform_specific_module
except ImportError:
    platform_specific_module = None

        无具体异常的 except 子句会捕获 SystemExitKeyboardInterrupt 异常,这会使使用 Control - C 中断程序变得困难,还可能掩盖其他问题。如果你想捕获所有表示程序错误的异常,应使用 except Exception:(无具体异常的 except 等同于 except BaseException:)。

一般来说,无具体异常的 except 子句应仅在以下两种情况下使用:

        1. 如果异常处理程序会打印或记录回溯信息,这样至少用户会知道发生了错误。

        2. 如果代码需要进行一些清理工作,然后再将异常向上抛出。这种情况下,使用 try...finally 语句可能是更好的处理方式。

  • 捕获操作系统错误时,优先使用 Python 3.3 引入的显式异常层次结构,而不是通过检查 errno 值来判断。
  • 此外,对于所有的 try/except 子句,应将 try 子句中的代码量限制到最少。这样做同样可以避免掩盖错误:
# 正确示例
try:
    value = collection[key]
except KeyError:
    return key_not_found(key)
else:
    return handle_value(value)
# 错误示例
try:
    # 范围太广!
    return handle_value(collection[key])
except KeyError:
    # 也会捕获 handle_value() 抛出的 KeyError
    return key_not_found(key)

当资源仅在代码的特定部分使用时,使用 with 语句确保资源在使用后能及时、可靠地清理。try...finally 语句也是可以接受的。

  • 如果上下文管理器除了获取和释放资源外还执行其他操作,应通过单独的函数或方法来调用它:
# 正确示例
with conn.begin_transaction():
    do_stuff_in_transaction(conn)
# 错误示例
with conn:
    do_stuff_in_transaction(conn)

后一个示例没有提供任何信息表明 __enter____exit__ 方法除了在事务结束后关闭连接外还做了其他事情。在这种情况下,明确表达很重要。

  • 返回语句要保持一致。函数中的所有返回语句要么都返回表达式,要么都不返回。如果有返回语句返回表达式,那么不返回值的返回语句应显式写成 return None,并且如果函数能执行到结尾,结尾处应有显式的返回语句:
# 正确示例
def foo(x):
    if x >= 0:
        return math.sqrt(x)
    else:
        return None

def bar(x):
    if x < 0:
        return None
    return math.sqrt(x)
# 错误示例
def foo(x):
    if x >= 0:
        return math.sqrt(x)

def bar(x):
    if x < 0:
        return
    return math.sqrt(x)

  • 检查字符串的前缀或后缀时,使用 startswith()endswith() 方法,而不是字符串切片。startswith()endswith() 方法更简洁,也更不容易出错:
# 正确示例
if foo.startswith('bar'):
# 错误示例
if foo[:3] == 'bar':
  • 比较对象类型时,应始终使用 isinstance() 函数,而不是直接比较类型:
# 正确示例
if isinstance(obj, int):
# 错误示例
if type(obj) is type(1):
  • 对于序列(字符串、列表、元组),利用空序列在布尔上下文中为假的特性:
# 正确示例
if not seq:
if seq:
# 错误示例
if len(seq):
if not len(seq):
  • 不要编写依赖于有实际意义的尾随空格的字符串字面量。这种尾随空格在视觉上难以区分,而且一些编辑器(或者最近的 reindent.py 脚本)会自动去除它们。
  • 不要使用 == 来比较布尔值和 TrueFalse
# 正确示例
if greeting:
# 错误示例
if greeting == True:
# 更糟糕的示例
if greeting is True:
  • 不建议在 try...finally 语句的 finally 子句中使用 returnbreakcontinue 等流程控制语句,因为这些语句会跳出 finally 子句。这是因为此类语句会隐式取消正在通过 finally 子句传播的任何活动异常:
# 错误示例
def foo():
    try:
        1 / 0
    finally:
        return 42

函数注解

随着 PEP 484 被采纳,函数注解的风格规则发生了变化。

函数注解应使用 PEP 484 规定的语法(上一节中有关于注解的一些格式建议)。

本 PEP 之前所建议的对注解风格进行试验的做法不再被鼓励。

不过,在标准库之外,现在鼓励在 PEP 484 规则范围内进行试验。例如,用 PEP 484 风格的类型注解标记一个大型的第三方库或应用程序,评估添加这些注解的难易程度,并观察它们是否能提高代码的可读性。

Python 标准库在采用此类注解时应持保守态度,但对于新代码和大规模重构,可以使用这些注解。

如果代码想以不同的方式使用函数注解,建议在文件顶部添加如下形式的注释:

# type: ignore

这会告诉类型检查器忽略所有注解。(更细致地禁用类型检查器警告的方法可在 PEP 484 中找到。)

和代码检查工具一样,类型检查器是可选的独立工具。Python 解释器默认不应因类型检查而发出任何消息,也不应根据注解改变其行为。

不想使用类型检查器的用户可以自由忽略它们。不过,预计第三方库包的用户可能希望对这些包运行类型检查器。为此,PEP 484 建议使用存根文件:类型检查器会优先读取 .pyi 文件而非对应的 .py 文件。存根文件可以随库一起分发,也可以(在获得库作者许可的情况下)通过 typeshed 仓库单独分发。

变量注解

PEP 526 引入了变量注解。其风格建议与上述函数注解的建议类似:

模块级变量、类变量、实例变量和局部变量的注解,冒号后面应有一个空格。

冒号前面不应有空格。

如果赋值语句有右侧值,等号两边应各有一个空格:

# 正确示例
code: int

class Point:
    coords: Tuple[int, int]
    label: str = '<unknown>'
# 错误示例
code:int  # 冒号后面没有空格
code : int  # 冒号前面有空格

class Test:
    result: int=0  # 等号两边没有空格

尽管 PEP 526 是针对 Python 3.6 被采纳的,但变量注解语法是所有 Python 版本存根文件的首选语法(详见 PEP 484)。

脚注

[1] 悬挂缩进是一种排版风格,即段落中除第一行外的所有行均缩进。在 Python 中,该术语用于描述这样一种风格:括号表达式的左括号位于行末(该行最后一个非空白字符为左括号),后续行进行缩进,直至遇到右括号。

参考文献

[2] 巴里(Barry)的 GNU Mailman 风格指南:http://barry.warsaw.us/software/STYLEGUIDE.txt
[3] 唐纳德・克努特(Donald Knuth)所著的《TeX 书》(The TeXBook),第 195 页和第 196 页。
[4] 驼峰命名法相关:http://www.wikipedia.com/wiki/Camel_case
[5] Typeshed 代码库:https://github.com/python/typeshed

最后更新时间 2025-02-01 07:28:42 GMT

翻译说明

本文为最新的Python官方的PEP8编码规范PEP 8 – Style Guide for Python Code 的中文翻译, 目的是练习一下自己的英文翻译能力,同时也分享给英语不是很流畅的开发人员参考。如果你的英文无障碍,建议还是直接阅读英语原文, 如有疑问欢迎交流讨论。

英文原文地址:https://peps.python.org/pep-0008/

Logo

中德AI开发者社区由X.Lab发起,旨在促进中德AI技术交流与合作,汇聚开发者及学者,共同探索前沿AI应用与创新。加入我们,共享资源,共创未来!🚀

更多推荐