于2023年8月翻译自PEP 8 – Style Guide for Python Code | peps.python.org除5个脚注或引用之外,中括号括起的内容为译者所加。排版尽力了,见谅。

介绍

本文档描述一个针对 Python 代码,包括主流Python发行版的标准库,的编码规范。对于Python的C语言实现的C语言编码风格规范,请参考PEP 7

这篇文档和  PEP 257 (关于“文档字符串”的规范) 由Guido的原有Python风格指导论文改编而来,加上了一些Barry的风格约定[2]。

这篇风格规范会由于语言[指Python]自身的改变带来的新约定增加、旧约定废弃而不断更新。

许多项目有他们自己的编码风格规范。如有任何冲突,以项目的风格规范优先。

放弃对一致性的盲目追求

uido的一个重要洞见是:代码更多地被读,更少地被写。这篇规范的意图在于提高代码的可读性,并让这种可读性在各种各样的Python代码间维持一致。就如PEP 20 [Python之禅] 所说,“可读性举足轻重”。

风格规范是关于一致性的规范。这份风格规范的一致性是重要的,项目内的一致性则更为重要,而单个模块或函数内的一致性是最重要的。

但是,要知道什么时候不一致——有时候规范推荐的做法并不适用。在模棱两可之时,遵从你最好的判断。看看其他的例子,决定哪样看起来最好。不要犹豫,尽管去问!

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

这是一些其他的忽视具体规范的理由:

  1. 应用规范会造成代码更加难读——甚至连熟悉这个PEP的人都觉得很难读。
  2. 为了跟周围同样不遵循知道的代码保持一致(也许出于历史原因)——尽管这也是个清理别人过失的机会(真正的极限编程风格)。
  3. 有问题的代码写在规范引入之前,而且没有[除代码风格之外的]其他理由去修改它们。
  4. 代码需要对旧版本的Python保持兼容性,而旧版本的Python不支持本风格规范推荐使用的特性。

代码布局

缩进

每级缩进使用4个空格。

延续行[指将过长的行分开写后得到的位于首行下面的那些行]应该与被括起来的元素垂直对齐,要么在圆括号、中括号或大括号内使用Python的隐式行连接,要么使用悬挂缩进[1]。当使用悬挂缩进时,应考虑:第一行不应有元素,延续行中更进一步的缩进需要清晰地区分出来:

# 正确:

# 与开放的界定符对齐.  [指跟括号对齐]
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语句太长了需要分行写的时候,值得注意的是这样一种结合:2个字符的关键词(比如 if),加上1个空格,加上一个为接下来的行创造自然的4空格缩进的圆括号。这个情况可能跟if语句里有缩进的代码产生视觉冲突,它们同样有自然的4空格缩进。这个PEP没有对在这种情况下如何区分if条件行和if声明的缩进这一问题采取明晰的立场,此种情况下可采取的方案包括但不限于:

# 不采用额外缩进 
[这就是上面说的情况,if条件也是4个空格缩进, if内的语句也是4个空格缩进,拼一块了]

if (this_is_one_thing and
    that_is_another_thing):
    do_something()

# 增加一句注释。注释可以在有高亮功能的编辑器里
# 创造分隔效果 [你看到的文档不一定有高亮]
if (this_is_one_thing and
    that_is_another_thing):
    # 两个条件都满足就可以做点什么
        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',
)

Tab还是空格?

推荐使用空格缩进。

Tab只有在需要与已经用Tab缩进了的代码保持一致时使用。

Python不允许混用Tab和空格。

行的最大长度

所有行的长度不超过79个字符。

对于大段、连续、没有结构约束的文字(比如docstring或者注释),行的长度不超过72个字符。

按照编辑器窗口宽度来限制行的长度让同时打开几个并排的文件变为可能,也为使用同时显示两个文件的代码审查工具提供了便利。

大多数工具的默认换行方式会破坏代码的视觉结构,使其更难以理解。这些限制[指关于行的最大长度的限制]被制定出来,用以避免在换行时编辑器设置为80个字符宽度时出现换行。即使换行时工具在最后一列放置一个标记符号,一些基于网络的工具也可能根本不提供动态换行功能。

有些团队强烈倾向于更长的行长度。对于仅仅或主要由一个团队来维护代码,如果团队对这个问题达成一致意见,可以将行长度限制增加到99个字符,前提是注释和文档字符串仍然在72个字符处进行换行。

保守起见,Python标准库要求将行长度限制在79个字符以内(文档字符串/注释限制在72个字符以内)。

在Python中,首选的换行方式是使用括号、方括号和大括号内的隐式换行。可以通过将表达式放在括号内来将过长的行分成多行。与使用反斜杠相比,应优先使用括号换行。

反斜杠仍然可能在某些情况下适用。例如,长的多个`with`语句在Python 3.10之前不能使用隐式换行,因此在该情况下可以使用反斜杠:

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 语句的使用。

请确保适当缩进续行。

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

几十年来,推荐的风格是在二元运算符之后换行。但是这种方式可能会以两种方式损害代码可读性:运算符分散在屏幕上的不同列中,并且每个运算符都被移动到前一行上,远离其操作数。在这种情况下,眼睛需要付出额外的工作来确定哪些项目是相加的,哪些是相减的:

# 错误:
# 运算符远离它们的操作数 
income = (gross_wages +
          taxable_interest +
          (dividends - qualified_dividends) -
          ira_deduction -
          student_loan_interest)

为了解决这个可读性问题,数学家和他们的出版商采用了相反的惯例。Donald Knuth在他的《计算机和排版》系列中解释了传统规则:“尽管段落中的公式总是在二元运算符和关系符之后换行,但显示的公式总是在二元运算符之前换行。”[3]

遵循数学家们的传统通常可以获得更易读的代码:

# 正确:
# 容易匹配操作符和它们的操作数 
income = (gross_wages
          + taxable_interest
          + (dividends - qualified_dividends)
          - ira_deduction
          - student_loan_interest)

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

空行

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

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

可以(尽量少用)使用额外的空行来分隔相关函数组。在一系列有关联的单行函数之间可以省略空行(例如一组dummy实现)。

在函数中要有节制地使用空行来划分逻辑部分。

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

源文件编码

核心Python发行版应总是使用UTF-8,且不应有编码声明。

[编码声明指文件第一行的# -*- coding: utf-8 -*-  这样的字符]

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

在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

标准库的代码应避免复杂的包布局并总是使用绝对导入。

  • 当从包含类的模块导入类时,这个写法通常是OK的:
from myclass import MyClass
from foo.bar.yourclass import YourClass

如果上面的写法造成了本地命名冲突,那就显式地写:

import myclass
import foo.bar.yourclass

 并使用“myclass.MyClass” 和 “foo.bar.yourclass.YourClass”。

  • 应避免使用通配符导入(即from <module> import *),因为它们会使命名空间中存在哪些名称变得不清楚,会让读者和许多自动化工具感到困惑。有一种情况可以为通配符导入辩护,那就是将内部接口重新发布为公共API的一部分(例如,用可选的加速器模块的定义覆盖纯Python实现的接口,并且事先不知道将覆盖哪些定义)。
    在这种方式下重新发布名称时,不论公共还是内部接口仍适用下面的规范。
  • 模块级别的”dunders”(即具有两个前导下划线和两个尾部下划线的名称),如__all____author____version__等,应该放在模块的文档字符串之后,但在除了__future__导入之外的任何导入语句之前。Python要求future导入必须出现在模块中除文档字符串之外的任何其他代码之前。
"""这是一个示例模块,

这个模块做一些事情。
"""

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本身)都有预提交钩子(pre-commit hooks)来拒绝它。
  • 始终在这些二元操作符的两侧都用单个空格包围:赋值(=),增量赋值(+=-=等),比较(==<>!=<><=>=innot inisis not),bool操作符(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: ...
  • 等号用于指示关键字参数或为没有注释的函数参数指定默认值时,不要在等号=周围使用空格:
# 正确:
def complex(real, imag=0.0):
    return magic(r=real, i=imag)
# 错误:
def complex(real, imag = 0.0):
    return magic(r = real, i = imag)

若结合使用参数注解与默认值,请在等号=周围使用空格:

# 正确:
def munge(sep: AnyStr = None): ...
def munge(input: AnyStr, sep: AnyStr = None, limit=1000): ...
# 错误:
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程序员请注意:请用英语写注释,除非你120%确定不会有使用其他语言的人阅读代码。

块注释

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

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

行内注释

有节制地使用行内注释。

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

解释显而易见的事情的行内注释是不必要的,甚至会分散注意力。别这样:

x = x + 1                 # x 自增 

但有时,这样有帮助:

x = x + 1                 # 补偿边界

[意思是边界从1开始,但 x 一开始是0]

文档字符串

编写好的文档字符串(也称为“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  单个大写字母
  • lowercase  小写字母
  • lower_case_with_underscores  有下划线的小写字母[又叫snake_case,蛇形命名]
  • UPPERCASE  大写字母
  • UPPER_CASE_WITH_UNDERSCORES  有下划线的大写字母
  • CapitalizedWords  首字母大写(又称 CapWords, 或者驼峰法 – 因为它高低起伏的样子[4])。它有时也被称作 StudlyCaps。
    注意:在CapWords中使用缩略词,请把缩略词的所有字母大写。所以,HTTPServerErrorHttpServerError更好。
  • mixedCase  与CapWords的不同之处在于最开始的字母是小写的[也叫小驼峰法]
  • Capitalized_Words_With_Underscores  丑爆了!
  • 还有一种将相关名称分组[意思是让人知道某些变量是相关的]的使用短小的唯一前缀的风格。这在 Python 中并不常用,但为了完整性而提到。例如,os.stat() 函数返回一个元组,其各个项传统上具有类似 st_modest_sizest_mtime 等的名称(这样做是为了强调与 POSIX 系统调用结构体字段的对应关系,有助于熟悉该结构的程序员)。

X11这个库[就是python-xlib, X client 相关的库,管理可视化窗口]使用X作为所有公共函数的前缀。通常认为这种风格是不必要的,因为在Python中,属性和方法名以对象为前缀,函数名以模块名为前缀。

补充一下,下面这些使用前导或后续下划线的特殊形式是被认可的(它们通常可以与任何大小写约定结合使用):

  • _single_leading_underscore:弱的“内部”使用指示符。 例如 from M import * 这个语句不会导入以单下划线开头的对象。
  • single_trailing_underscore_:根据惯例用来避免与 Python 关键字冲突,例如
    tkinter.Toplevel(master, class_='ClassName')
  • __double_leading_underscore:在命名类属性时,用于引入名称修饰(name mangling) (在类FooBar中, __boo 变为 _FooBar__boo;见下述)。
  • __double_leading_and_trailing_underscore__: “魔法” 对象或属性, 存在于用户控制命名空间中。 例如  __init__, __import__ 或者 __file__ 。 绝对不要自己发明这样的命名, 请仅在文档中使用它们。

指示性的:命名约定

需要避免的名字

绝不使用单个 ‘l’ (小写的L)、’O’ (大写的O) 或者 ‘I’ (大写的i)作为单字母变量名。

在一些字体中,它们很容易跟数字 1 或 0 混淆。如果你真的很想用 l , 请使用 L 。

ASCII 兼容性

如同PEP 3131中的policy section所说,标准库中的变量命名必须是ASCII兼容的。

包和模块的命名

模块应有简短且全小写的名字。如果能提高可读性,可以在模块的名字中使用下划线。Python 的包也应该有简短且全小写的名字,但不鼓励使用下划线。

当一个C/C++编写的模块有附加的、更高级的Python接口时,C/C++模块的名字应有单个先导下划线(例如 _socket)。

类的命名

一般而言,类的名字应采用 CapWords 约定。

若[类是]有文档且主要被用作可调用对象的接口,也可以使用函数的命名约定。

值得注意的是,内置名称[类内部的名称]有一个单独的命名约定:大多数内置名称是一个单词(或两个单词连在一起),只有在异常名称和内置常量中才使用 CapWords 约定。

类型变量名

PEP 484 中引入的类型变量的名字一般来说应使用 CapWords,并优先简短的名称:T, AnyStr, Num。推荐为变量加上相应的后缀 _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 作为类方法的第一个参数。

如果函数的参数名与Python保留字相冲突[也就是相同,撞名字了],一般来说加上尾部下划线比使用缩写或者错误拼写要好。所以,class_clss 要好。(也许更好的做法是使用同义词避开冲突。)

方法的名称和实例变量

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

仅为非公开的方法和非公开的实例使用单个先导下划线。

为了避免与子类的名称冲突,使用两个先导下划线来引入名称修饰规则。

Python会将这些名字用类的名称修饰:如果类Foo有一个叫做__a的属性,它就不能通过Foo.__a被访问。(如果用户执意要访问,使用Foo._Foo__a是可以访问到的。)一般而言,两个先导下划线应该仅仅被用来避免与类中设计为子类的属性产生冲突。

注意:有一些关于 __names [即两个先导下划线的名字] 的争论(下述)。

常量

常量通常是定义为模块级别的,使用全大写字母并以下划线分隔单词。例子包括MAX_OVERFLOWTOTAL

为继承设计

一定要决定好一个类的方法和实例变量(统称为“属性”)是公开的(public)还是非公开的(non-public)。如果不确定,选择非公开的属性。这样会比选择公开的属性更容易,因为非公开属性可以之后再转为公开,而把公开属性转为非公开则更困难。

公开属性是指你希望与你的类无关的客户端使用的属性,并且你承诺避免不向后兼容的更改。非公开属性是指不打算由第三方使用的属性;你不保证非公开属性不会被更改或甚至被删除。

我们不使用“私有”一词,因为在Python中没有真正的“私有”属性(除非做了一些通常不必要的工作)。

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

如此一来,Pythonic 的指导原则是:

  • 公开属性不应该有先导下划线。
  • 如果公开属性跟Python保留字冲突了,为公开属性的名字加上单个尾部下划线,这比使用简写或错误拼写更好。(但是,尽管有这条规则,cls对于任何已知为类的变量和参数都是更好的,特别是在作为类方法的第一个参数时)
    • 注意 1: 参见上文中对于类方法的推荐命名做法。
  • 对于简单的公开数据属性,最好只暴露属性名,而不要暴露复杂的存取、修改方法[一种 Java 风格的做法,不推荐用于Python]。如果你发现简单的数据属性需要增加功能行为,记住,Python为未来的优化留了一条简单的路。在这种情况下,使用属性来将功能实现隐藏在简单的数据属性访问语法后面。
    • 注意1:尽量让函数行为没有副作用,虽然缓存之类副作用通常可以接受。
    • 注意2:避免在属性中使用需要消耗大量计算资源的操作;属性访问符号使调用者相信访问是(相对)便宜的。
  • 如果你的类打算被子类化,并且你有一些你不希望子类使用的属性,考虑将它们的名称加上两个先导下划线并且不加尾部下划线。这会触发Python的名称修饰算法,将类名修饰到属性名中。这有助于避免子类意外包含与属性名相同的属性时出现属性名冲突。
    • 注意1:只有简单的类名会在名称修饰中使用,所以如果子类选择了相同的类名和属性名,仍然可能发生名称冲突。注意2:名称修饰确实会让一些用法,例如调试和__getattr__(),变得没那么方便。但是名称修饰算法有良好的文档且易于手动实现。
    • 注意3:不是谁都喜欢名称修饰。尝试平衡避免意外命名冲突的需求与高级调用者潜在使用的可能性。[意思是,尝试既不产生命名冲突(不使用名称修饰),又让调用者舒服]

外部和内部接口

任何向后兼容承诺仅仅对公开接口生效。所以,用户能清晰地分辨内部和外部接口是重要的。

有文档的接口通常被认为是外部接口,除非文档明确说明接口为临时接口或内部接口,以排除在向后兼容承诺之外。所有没有文档的接口都被认为是内部接口。

为了更好的反射(introspection)效果,模块应该使用__all__属性明确声明它们的公共API的名字。将__all__设置为空列表以表示模块没有公共接口。

即使恰当地设置了__all__,内部接口(包、模块、类、函数、属性或其他名称)扔应该以单个先导下划线开头。

如果一个接口包含内部的命名空间,那么这个接口也被认为是内部的。

始终应改把导入的名称视为实现细节。除非它们是包含模块的有明确文档的 API 的一部分(如os.path 或将子模块的功能暴露给 __init__ 模块的包),否则其他模块不得依赖于对这些导入名称的间接访问。

编程建议

  • 代码不得用对 Python 的其他实现(如 PyPy、Jython、IronPython、Cython、Psyco 等)造成不利影响的方式编写。
    例如,不要仰仗CPython对于a += ba = a + b 之类表达式中字符串就地拼接的高效实现。这个优化即使在CPython中也是不可靠的(仅仅对几个类型生效),并且所有不使用索引计数(refcounting)的实现都没有这个优化。在库中对性能敏感的部分,应改用''.join()。这样能保证在各种实现中拼接的表现都是线性的。
  • 与单例,比如None,的比较,应总是使用 isis not 完成,而不是等于号。
  • 另外,在你想表达 if x is not None 时写 if x,一定要小心。例如,当要测试一个默认值是None的变量或参数是否被赋了别的值的时候。别的值(例如一个容器)也可能是在布尔化的上下文中为False的![比如对空列表,if xif x is not 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 > x 换成 x < y, 把 y >= x 换成 x <= ysort() 方法和 min() 方法被保证使用 < 运算符,max() 方法被保证使用 > 运算符。但最好还是实现所有六种操作,这样才不会在其他情况下产生混淆。
  • 总是使用 def 语句,不要使用赋值语句直接把lambda表达式与标识符绑定:
# 正确:
def f(x): return 2*x
# 错误:
f = lambda x: 2*x

第一种形式表示生成的函数对象的名称特定为 ‘f’,而不是通用的 ‘<lambda>’。这在追踪(tracebacks)和一般的字符串表示中更有用。使用赋值语句消除了 lambda 表达式对于显式的 def 语句上的唯一优势(即它可以嵌入到更大的表达式中)。

  • Exception继承,而不是 BaseException。直接继承自 BaseException 仅为那些捕获它们总是件错事[也就是不应该捕获]的异常保留。
    [这样理解:BaseException是那些你用 except Exception as exc 都不该捕获到的错误]
    设计异常的层次结构时,更需要[考虑]代码捕获异常时需要的差异,而不是异常在何处发生。考虑用程序回答“发生了什么”这个问题,而不是只说“有麻烦事来了”(参考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块中保留所需要的最少的语句。重复一遍,这可以避免掩盖bug:
# 正确:
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 语句保持一致。要么一个函数中的所有return 语句都应该返回一个表达式,要么这个函数中没有一个return 语句应该返回表达式。如果每个return 语句都返回表达式,所有没有值可以返回的return 语句应该明确地说明返回None,并且函数最后应该有明确的return语句(如果可达):
# 正确:

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):
  • 对于序列(字符串,列表,元组),利用空序列为False这个特点:
# 正确:
if not seq:
if seq:
# 错误:
if len(seq):
if not len(seq):
  • 不要写显式依赖于尾部空格的字符串。这样的尾部空格在视觉上难以辨认,而且会被一些编辑器(或者最近的,reindent.py)截掉。
  • 不要用==比较布尔变量和 True 或 False:

# 正确:
if greeting:
# 错误:
if greeting == True:

糟糕的:

# 错误:
if greeting is True:
  • 不鼓励在 try…finally 的 finally 子句中使用 return/break/continue 控制流语句跳出 finally 子句的范围。这是因为这种语句会隐式地取消通过 finally 子句传播的活动的异常[就是有个异常抛出来了来着,结果你一个return跳走了]:
# 错误:
def foo():
    try:
        1 / 0
    finally:
        return 42

函数注释

随着对于PEP 484 的接受,函数注释风格的规则变了。

  • 函数注释应采用PEP 484 语法(前几节有关于注释格式的推荐)。
  • 这个PEP 中之前的注释风格的试验现在已经不鼓励。
  • 然而,在标准库之外,鼓励按照 PEP 484  的规则进行实验。例如,使用PEP 484 风格的类型注解标记一个大型的第三方库或应用程序,评估在添加这些注解时的难易程度,并观察它们的存在是否提高了代码的可理解性。
  • 在接受这些注释这件事情上,python标准库应采取保守态度,但允许它们在新代码和大的重构中的使用。
  • 对于希望在函数注释中采用不同用途的代码,建议使用以下形式的注释:
# type: ignore

加在文件中靠近顶部的位置;它告诉类型检查器( type checker)忽略所有注释。(粒度更细的禁用类型检查器的办法可以在PEP 484 中找到。)

  • 类型检查器就像Linter,是可选的、独立的工具。默认情况下Python解释器不应提示任何由类型检查产生的信息,也不应基于类型检查改变行为。
  • 不想用类型检查的用户大可以忽略它们。但是,可预见的是第三方库的用户想对第三方库使用类型检查器。为此PEP 484 推荐使用桩文件(stub file):.pyi 会比相应的.py文件更优先被被类型检查器读取。桩文件可以与库一同发布,也可以单独地(征得库作者的授权)通过typeshed repo [5]发布。

变量注释

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的桩文件都推荐采用类型注释语法。

脚注:

[1]悬挂缩进是一种排版风格,指的是段落中除了第一行之外的所有行都进行缩进。在 Python 的上下文中,该术语用于描述一种风格,即括号语句的开头括号是行中最后一个非空格字符,后续的行进行缩进直到闭合括号。

引用

[2]Barry’s GNU Mailman style guide http://barry.warsaw.us/software/STYLEGUIDE.txt

[3]Donald Knuth’s The TeXBook, pages 195 and 196.

[4]http://www.wikipedia.com/wiki/CamelCase

[5]Typeshed repo GitHub – python/typeshed: Collection of library stubs for Python, with static types

著作权

这篇文档被归入公共领域。

最后修改日期: 2023年9月1日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。