Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 8 – Python 代码风格指南

作者:
Guido van Rossum <guido at python.org>, Barry Warsaw <barry at python.org>, Alyssa Coghlan <ncoghlan at gmail.com>
状态:
活跃
类型:
流程
创建时间:
2001 年 7 月 5 日
历史记录:
2001 年 7 月 5 日,2013 年 8 月 1 日

目录

介绍

本文档提供 Python 代码的编码约定,这些代码构成了 Python 主发行版中的标准库。请参阅关于 C 代码的风格指南 的配套信息 PEP。

本文档和 PEP 257(文档字符串约定)改编自 Guido 的原始 Python 风格指南文章,并添加了 Barry 的风格指南 [2] 中的一些内容。

随着识别出更多约定,以及语言本身的更改使过去的约定过时,这份风格指南会随着时间的推移而发展。

许多项目都有自己的编码风格指南。在出现任何冲突的情况下,这些项目特定的指南优先于该项目。

愚蠢的一致性是小人物的谬误

Guido 的一项重要见解是,代码被阅读的次数远多于被编写的次数。这里提供的指南旨在提高代码的可读性,并在各种 Python 代码中保持一致。正如 PEP 20 所说,“可读性很重要”。

风格指南是关于一致性。与这份风格指南保持一致很重要。在一个项目内保持一致性更重要。在一个模块或函数内保持一致性是最重要的。

但是,要懂得什么时候不一致——有时风格指南的建议并不适用。有疑问时,请使用你最好的判断力。查看其他示例并决定什么看起来最好。不要犹豫去问!

特别是:不要为了遵守这个 PEP 而破坏向后兼容性!

其他一些忽略特定指南的正当理由

  1. 当应用指南会降低代码可读性时,即使是习惯阅读遵循本 PEP 的代码的人也会感到难以理解。
  2. 为了与周围也违反该指南的代码保持一致(可能出于历史原因)——尽管这也可能是一个清理他人烂摊子的机会(以真正的 XP 风格)。
  3. 因为该代码早于该指南的引入,并且没有其他理由修改该代码。
  4. 当代码需要与不支持风格指南推荐的功能的旧版 Python 保持兼容时。

代码布局

缩进

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

延续行应通过 Python 在括号、方括号和花括号内的隐式行连接,或使用 *悬挂缩进* [1] 垂直对齐包装元素。使用悬挂缩进时,应考虑以下几点:第一行不应该有参数,并且应该使用进一步的缩进以清楚地区分它作为延续行

# Correct:

# Aligned with opening delimiter.
foo = long_function_name(var_one, var_two,
                         var_three, var_four)

# Add 4 spaces (an extra level of indentation) to distinguish arguments from the rest.
def long_function_name(
        var_one, var_two, var_three,
        var_four):
    print(var_one)

# Hanging indents should add a level.
foo = long_function_name(
    var_one, var_two,
    var_three, var_four)
# Wrong:

# Arguments on first line forbidden when not using vertical alignment.
foo = long_function_name(var_one, var_two,
    var_three, var_four)

# Further indentation required as indentation is not distinguishable.
def long_function_name(
    var_one, var_two, var_three,
    var_four):
    print(var_one)

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

可选

# Hanging indents *may* be indented to other than 4 spaces.
foo = long_function_name(
  var_one, var_two,
  var_three, var_four)

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

# No extra indentation.
if (this_is_one_thing and
    that_is_another_thing):
    do_something()

# Add a comment, which will provide some distinction in editors
# supporting syntax highlighting.
if (this_is_one_thing and
    that_is_another_thing):
    # Since both conditions are true, we can frobnicate.
    do_something()

# Add some extra indentation on the conditional continuation line.
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())

(请参阅之前关于 多行 if 语句 的讨论,以进一步了解此类多行 with 语句的缩进。)

另一个例子是 assert 语句。

确保正确缩进延续行。

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

几十年来,推荐的风格是在二元运算符之后换行。但这会从两个方面影响可读性:运算符往往分散在屏幕的不同列中,并且每个运算符都被移离其操作数并转移到上一行。这里,眼睛需要做额外的工作来判断哪些项目被加了,哪些项目被减了

# Wrong:
# operators sit far away from their operands
income = (gross_wages +
          taxable_interest +
          (dividends - qualified_dividends) -
          ira_deduction -
          student_loan_interest)

为了解决这个问题,数学家和他们的出版商遵循相反的约定。唐纳德·克努斯在他的 *计算机与排版* 系列中解释了传统规则:“虽然段落中的公式总是二元运算和关系之后换行,但显示的公式总是二元运算之前换行” [3].

遵循数学中的传统通常会产生更易读的代码

# Correct:
# easy to match operators with operands
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

在 Python 代码中,可以在二元运算符之前或之后换行,只要约定在本地保持一致即可。对于新代码,建议使用克努斯风格。

空行

用两个空行包围顶层函数和类定义。

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

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

在函数中适度地使用空行来指示逻辑部分。

Python 接受 control-L(即 ^L)换页符作为空格;许多工具将这些字符视为页面分隔符,因此你可以使用它们来分隔文件相关部分的页面。注意,某些编辑器和基于 Web 的代码查看器可能不识别 control-L 作为换页符,并会在其位置显示另一个符号。

源文件编码

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

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

Python 标准库中的所有标识符都必须使用 ASCII 标识符,并且应该尽可能使用英文单词(在许多情况下,会使用非英文的缩写和专业术语)。

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

导入

  • 导入通常应该在单独的行上
    # Correct:
    import os
    import sys
    
    # Wrong:
    import sys, os
    

    不过也可以这样说

    # Correct:
    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__ 导入语句。

"""This is the example module.

This module does stuff.
"""

from __future__ import barry_as_FLUFL

__all__ = ['a', 'b', 'c']
__version__ = '0.1'
__author__ = 'Cardinal Biggles'

import os
import sys

字符串引号

在 Python 中,单引号字符串和双引号字符串是相同的。本 PEP 不对此提出建议。选择一个规则并坚持下去。但是,当字符串包含单引号或双引号字符时,使用另一个引号以避免字符串中的反斜杠。这将提高可读性。

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

表达式和语句中的空格

个人偏好

在以下情况下避免多余的空格

  • 在括号、方括号或花括号的内部
    # Correct:
    spam(ham[1], {eggs: 2})
    
    # Wrong:
    spam( ham[ 1 ], { eggs: 2 } )
    
  • 在尾随逗号和后面的右括号之间
    # Correct:
    foo = (0,)
    
    # Wrong:
    bar = (0, )
    
  • 在逗号、分号或冒号之前
    # Correct:
    if x == 4: print(x, y); x, y = y, x
    
    # Wrong:
    if x == 4 : print(x , y) ; x , y = y , x
    
  • 但是,在切片中,冒号充当二元运算符,两侧应具有相同数量的空格(将其视为优先级最低的运算符)。在扩展切片中,两个冒号必须应用相同的空格量。例外:当切片参数省略时,空格也会省略。
    # Correct:
    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]
    
    # Wrong:
    ham[lower + offset:upper + offset]
    ham[1: 9], ham[1 :9], ham[1:9 :3]
    ham[lower : : step]
    ham[ : upper]
    
  • 在开始函数调用参数列表的左括号之前
    # Correct:
    spam(1)
    
    # Wrong:
    spam (1)
    
  • 在开始索引或切片的左括号之前
    # Correct:
    dct['key'] = lst[index]
    
    # Wrong:
    dct ['key'] = lst [index]
    
  • 在赋值(或其他)运算符周围使用多个空格来使其与另一个运算符对齐
    # Correct:
    x = 1
    y = 2
    long_variable = 3
    
    # Wrong:
    x             = 1
    y             = 2
    long_variable = 3
    

其他建议

  • 避免任何地方的尾随空格。由于它通常是不可见的,因此可能会造成混淆:例如,反斜杠后跟一个空格和一个换行符不算作行延续标记。一些编辑器不会保留它,许多项目(如 CPython 本身)都有预提交钩子来拒绝它。
  • 始终在以下二元运算符两侧使用单个空格:赋值(=)、增强赋值(+=-= 等)、比较(==<>!=<><=>=innot inisis not)、布尔值(andornot)。
  • 如果使用优先级不同的运算符,请考虑在优先级最低的运算符周围添加空格。使用您自己的判断;但是,永远不要使用超过一个空格,并且始终在二元运算符的两侧使用相同数量的空格。
    # Correct:
    i = i + 1
    submitted += 1
    x = x*2 - 1
    hypot2 = x*x + y*y
    c = (a+b) * (a-b)
    
    # Wrong:
    i=i+1
    submitted +=1
    x = x * 2 - 1
    hypot2 = x * x + y * y
    c = (a + b) * (a - b)
    
  • 函数注释应使用冒号的正常规则,并且如果存在,始终在 -> 箭头周围使用空格。(有关函数注释的更多信息,请参见下面的 函数注释。)
    # Correct:
    def munge(input: AnyStr): ...
    def munge() -> PosInt: ...
    
    # Wrong:
    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): ...
    
  • 通常不鼓励使用复合语句(同一行上的多个语句)。
    # Correct:
    if foo == 'blah':
        do_blah_thing()
    do_one()
    do_two()
    do_three()
    

    而不是

    # Wrong:
    if foo == 'blah': do_blah_thing()
    do_one(); do_two(); do_three()
    
  • 虽然有时可以将带有小语体的 if/for/while 放在同一行,但对于多语句语句永远不要这样做。此外,避免折叠这种长行!

    而不是

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

    绝对不要

    # Wrong:
    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()
    

何时使用尾随逗号

尾随逗号通常是可选的,但它们在创建单个元素的元组时是必需的。为了清晰起见,建议将后者包围在(技术上是多余的)括号中。

# Correct:
FILES = ('setup.cfg',)
# Wrong:
FILES = 'setup.cfg',

当尾随逗号是多余的时,它们在使用版本控制系统时通常很有用,当预期值、参数或导入项目的列表会随着时间的推移而扩展时。模式是将每个值(等)放在单独的行上,始终添加尾随逗号,并在下一行添加右括号/方括号/花括号。但是,在同一行上与结束分隔符一起添加尾随逗号没有意义(除了上面提到的单个元素元组的情况)。

# Correct:
FILES = [
    'setup.cfg',
    'tox.ini',
    ]
initialize(FILES,
           error=True,
           )
# Wrong:
FILES = ['setup.cfg', 'tox.ini',]
initialize(FILES, error=True,)

注释

与代码相矛盾的注释比没有注释更糟糕。当代码更改时,始终优先考虑使注释保持最新!

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

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

在多句注释中,应在句号后使用一个或两个空格,但最后一句除外。

确保您的注释清晰易懂,方便您所用语言的其他使用者理解。

来自非英语国家的 Python 编码人员:请用英语编写您的注释,除非您 120% 确定该代码永远不会被不讲您语言的人阅读。

块注释

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

块注释内的段落用包含单个 # 的行隔开。

行内注释

谨慎使用内联注释。

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

如果内联注释说明了显而易见的事情,那么它们是不必要的,实际上会分散注意力。不要这样做

x = x + 1                 # Increment x

但有时,这很有用

x = x + 1                 # Compensate for border

文档字符串

编写良好文档字符串(又称“docstrings”)的约定在 PEP 257 中永垂不朽。

  • 为所有公共模块、函数、类和方法编写文档字符串。非公共方法不需要文档字符串,但您应该有一个注释来描述该方法的功能。此注释应出现在 def 行之后。
  • PEP 257 描述了良好的文档字符串约定。请注意,最重要的是,结束多行文档字符串的 """ 应位于单独的一行上。
    """Return a foobang
    
    Optional plotz says to frobnicate the bizbaz first.
    """
    
  • 对于单行文档字符串,请将结束的 """ 保持在同一行上。
    """Return an ex-parrot."""
    

命名约定

Python 库的命名约定有点混乱,因此我们永远无法使其完全一致——尽管如此,以下还是当前推荐的命名标准。新模块和包(包括第三方框架)应按照这些标准编写,但在现有库采用不同风格的情况下,优先考虑内部一致性。

首要原则

对用户可见的 API 公共部分的名称应遵循反映用法而不是实现的约定。

描述性:命名风格

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

以下命名风格通常有所区分

  • b(单个小写字母)
  • B(单个大写字母)
  • 小写
  • 带下划线的_小写
  • 大写
  • 带下划线的_大写
  • CapitalizedWords(或 CapWords 或 CamelCase——因此得名,因为它的字母看起来像驼峰 [4])。这有时也被称为 StudlyCaps。

    注意:在 CapWords 中使用缩写时,将缩写的所有字母都大写。因此 HTTPServerError 比 HttpServerError 更好。

  • 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__。不要发明此类名称;仅在文档中使用它们。

规定性:命名约定

要避免的名称

永远不要使用字符“l”(小写字母 el)、“O”(大写字母 oh)或“I”(大写字母 eye)作为单字符变量名。

在某些字体中,这些字符与数字一和零难以区分。当你想要使用“l”时,请改用“L”。

ASCII 兼容性

标准库中使用的标识符必须与 PEP 3131策略部分 中所述的 ASCII 兼容。

包和模块名称

模块应该具有简短的、全部小写的名称。如果可以提高可读性,可以在模块名称中使用下划线。Python 包也应该具有简短的、全部小写的名称,尽管不鼓励使用下划线。

当用 C 或 C++ 编写的扩展模块有一个提供更高层(例如面向对象更多)接口的配套 Python 模块时,C/C++ 模块前面有一个下划线(例如 _socket)。

类名

类名通常应该使用 CapWords 约定。

在接口已记录且主要用作可调用对象的情况下,可以使用函数命名约定。

请注意,内置名称有一个单独的约定:大多数内置名称是单个单词(或两个连在一起的单词),仅在异常名称和内置常量中使用 CapWords 约定。

类型变量名称

PEP 484 中引入的类型变量的名称通常应该使用 CapWords,并优先使用短名称:TAnyStrNum。建议为用于声明协变或逆变行为的变量分别添加后缀 _co_contra

from typing import TypeVar

VT_co = TypeVar('VT_co', covariant=True)
KT_contra = TypeVar('KT_contra', contravariant=True)

异常名称

由于异常应该是类,因此这里适用类命名约定。但是,你应该在你的异常名称上使用后缀“Error”(如果异常实际上是一个错误)。

全局变量名称

(希望这些变量是仅供在一个模块内使用。)约定与函数的约定基本相同。

旨在通过 from M import * 使用的模块应使用 __all__ 机制来防止导出全局变量,或者使用较旧的约定在这些全局变量前面加一个下划线(你可能想要这样做以表明这些全局变量是“模块非公开”的)。

函数和变量名称

函数名应该小写,必要时用下划线分隔单词以提高可读性。

变量名遵循与函数名相同的约定。

仅在已存在的风格中允许 mixedCase(例如 threading.py),以保留向后兼容性。

函数和方法参数

始终使用 self 作为实例方法的第一个参数。

始终使用 cls 作为类方法的第一个参数。

如果函数参数的名称与保留关键字冲突,通常最好在后面添加一个下划线,而不是使用缩写或拼写错误。因此,class_clss 更好。(也许更好的做法是使用同义词来避免此类冲突。)

方法名称和实例变量

使用函数命名规则:小写,必要时用下划线分隔单词以提高可读性。

仅使用一个前导下划线表示非公开方法和实例变量。

为了避免与子类发生名称冲突,请使用两个前导下划线来调用 Python 的名称混淆规则。

Python 会用类名混淆这些名称:如果类 Foo 有一个名为 __a 的属性,则无法通过 Foo.__a 访问它。(坚持的用户仍然可以通过调用 Foo._Foo__a 来访问它。)通常,仅在需要避免与旨在被子类化的类的属性发生名称冲突时才应使用双前导下划线。

注意:关于使用 __names 的方式存在一些争议(见下文)。

常量

常量通常在模块级别定义,并以全大写字母书写,下划线分隔单词。例如 MAX_OVERFLOWTOTAL

为继承而设计

始终确定类的所有方法和实例变量(统称为“属性”)应该是公开的还是非公开的。如有疑问,请选择非公开;以后将其设为公开比将公开属性设为非公开更容易。

公开属性是你期望类的不相关客户端使用的属性,你承诺避免向后不兼容的更改。非公开属性是那些不打算被第三方使用的属性;你不对非公开属性不会更改或甚至不会被删除做出任何保证。

我们这里不使用术语“私有”,因为在 Python 中没有属性是真正私有的(除非进行大量不必要的操作)。

属性的另一类是那些属于“子类 API”的属性(在其他语言中通常被称为“受保护”的属性)。一些类被设计为从它们继承,以扩展或修改类的行为方面。在设计此类类时,请注意明确决定哪些属性是公开的,哪些是子类 API 的一部分,哪些是真正仅供基类使用。

考虑到这一点,以下是 Python 式指南

  • 公开属性不应有任何前导下划线。
  • 如果你的公开属性名称与保留关键字冲突,请在属性名称后面添加一个尾部下划线。这优于缩写或拼写错误。(但是,尽管有此规则,“cls”是已知为类的任何变量或参数的首选拼写,尤其是类方法的第一个参数。)

    注意 1:请参阅上面针对类方法的参数名称建议。

  • 对于简单的公开数据属性,最好只公开属性名称,而没有复杂的访问器/修改器方法。请记住,Python 提供了一种简单的路径来进行将来的增强,如果你发现简单的数据属性需要扩展功能行为。在这种情况下,使用属性将功能实现隐藏在简单的数据属性访问语法后面。

    注意 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,而你实际的意思是 if x is not None - 例如,当测试默认值为 None 的变量或参数是否被设置为其他值时。另一个值可能具有类型(如容器),该类型在布尔上下文中可能是假的!

  • 使用 is not 运算符,而不是 not ... is。虽然这两个表达式在功能上相同,但前者更易读,更可取。
    # Correct:
    if foo is not None:
    
    # Wrong:
    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 表达式直接绑定到标识符的赋值语句。
    # Correct:
    def f(x): return 2*x
    
    # Wrong:
    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: 子句将捕获 SystemExit 和 KeyboardInterrupt 异常,这使得使用 Control-C 中断程序变得更加困难,并且可能会掩盖其他问题。如果你想捕获所有表示程序错误的异常,请使用 except Exception:(裸 except 等效于 except BaseException:)。

    一个好的经验法则是将裸“except”子句的用法限制在两种情况下。

    1. 如果异常处理程序将打印或记录回溯;至少用户会意识到发生了错误。
    2. 如果代码需要执行一些清理工作,但随后让异常使用 raise 向上传播。 try...finally 可以是处理这种情况的更好方法。
  • 在捕获操作系统错误时,优先使用 Python 3.3 中引入的显式异常层次结构,而不是对 errno 值进行内省。
  • 此外,对于所有 try/except 子句,将 try 子句限制在绝对必要的最小代码量。同样,这可以避免掩盖错误。
    # Correct:
    try:
        value = collection[key]
    except KeyError:
        return key_not_found(key)
    else:
        return handle_value(value)
    
    # Wrong:
    try:
        # Too broad!
        return handle_value(collection[key])
    except KeyError:
        # Will also catch KeyError raised by handle_value()
        return key_not_found(key)
    
  • 当资源是特定代码部分的本地资源时,使用 with 语句来确保它在使用后立即可靠地清理。try/finally 语句也是可以接受的。
  • 上下文管理器应通过单独的函数或方法调用,无论何时它们执行的操作不仅仅是获取和释放资源。
    # Correct:
    with conn.begin_transaction():
        do_stuff_in_transaction(conn)
    
    # Wrong:
    with conn:
        do_stuff_in_transaction(conn)
    

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

  • 在 return 语句中保持一致性。函数中的所有 return 语句都应返回表达式,或者都不应返回表达式。如果任何 return 语句返回表达式,则任何不返回值的 return 语句都应明确声明为 return None,并且函数末尾(如果可达)应存在显式 return 语句。
    # Correct:
    
    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)
    
    # Wrong:
    
    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() 更简洁,更不容易出错。

    # Correct:
    if foo.startswith('bar'):
    
    # Wrong:
    if foo[:3] == 'bar':
    
  • 对象类型比较应始终使用 isinstance(),而不是直接比较类型。
    # Correct:
    if isinstance(obj, int):
    
    # Wrong:
    if type(obj) is type(1):
    
  • 对于序列(字符串、列表、元组),使用空序列为假这一事实。
    # Correct:
    if not seq:
    if seq:
    
    # Wrong:
    if len(seq):
    if not len(seq):
    
  • 不要编写依赖于重要尾随空格的字符串字面量。这种尾随空格在视觉上无法区分,一些编辑器(或者最近的 reindent.py)会删除它们。
  • 不要使用 == 将布尔值与 True 或 False 进行比较。
    # Correct:
    if greeting:
    
    # Wrong:
    if greeting == True:
    

    更糟糕的是

    # Wrong:
    if greeting is True:
    
  • 不鼓励在 try...finally 的 finally 子句中使用流控制语句 return/break/continue,其中流控制语句将跳出 finally 子句。这是因为这些语句将隐式取消任何正在通过 finally 子句传播的活动异常。
    # Wrong:
    def foo():
        try:
            1 / 0
        finally:
            return 42
    

函数注解

随着 PEP 484 的接受,函数注释的样式规则已更改。

  • 函数注释应使用 PEP 484 语法(之前部分中有一些针对注释的格式建议)。
  • 以前在此 PEP 中推荐的注释样式的实验不再鼓励。
  • 但是,在 stdlib 之外,鼓励在 PEP 484 规则范围内进行实验。例如,使用 PEP 484 样式类型注释标记大型第三方库或应用程序,查看添加这些注释的难易程度,以及观察它们的存在是否提高了代码的可理解性。
  • Python 标准库应谨慎采用这些注释,但允许在新代码和大型重构中使用它们。
  • 对于希望对函数注释进行不同使用的代码,建议添加以下形式的注释
    # type: ignore
    

    在文件顶部附近;这告诉类型检查器忽略所有注释。(在 PEP 484 中可以找到更细粒度的禁用类型检查器抱怨的方法)。

  • 与 linter 一样,类型检查器是可选的、独立的工具。Python 解释器默认情况下不应因类型检查而发出任何消息,也不应根据注释更改其行为。
  • 不想使用类型检查器的用户可以自由地忽略它们。但是,预计第三方库包的用户可能希望对这些包运行类型检查器。为此,PEP 484 建议使用存根文件:.pyi 文件,类型检查器优先于相应的 .py 文件读取这些文件。存根文件可以与库一起分发,也可以单独分发(在库作者的许可下)通过 typeshed 仓库 [5]

变量注解

PEP 526 引入了变量注释。它们的样式建议类似于上面描述的函数注释。

  • 模块级变量、类和实例变量以及局部变量的注释在冒号后应该有一个空格。
  • 冒号前不应该有空格。
  • 如果赋值有右侧,则等号两边应恰好有一个空格。
    # Correct:
    
    code: int
    
    class Point:
        coords: Tuple[int, int]
        label: str = '<unknown>'
    
    # Wrong:
    
    code:int  # No space after colon
    code : int  # Space before colon
    
    class Test:
        result: int=0  # No spaces around equality sign
    
  • 尽管 PEP 526 被 Python 3.6 接受,但变量注释语法是所有 Python 版本的存根文件的首选语法(有关详细信息,请参见 PEP 484)。

脚注

参考资料


来源:https://github.com/python/peps/blob/main/peps/pep-0008.rst

最后修改时间:2024-09-09 14:02:27 GMT