当前位置: 首页 > 工具软件 > PEP > 使用案例 >

PEP(Python Enhancement Proposals, python改进建议书)8--python代码风格指南

濮彬
2023-12-01

介绍

本文档给出了包含主要 Python 发行版中标准库的 Python 代码的编码约定。请参阅描述 Python 的 C 实现中的 C 代码的样式指南的配套信息 PEP

本文档和PEP257(Docstring约定)改编自Guido最初的Python Style Guide文章,并添加了Barry的Style Guide[2]

随着附加约定的确定,以及过去的约定因语言本身的变化而过时,本样式指南会随着时间的推移而演变。

许多项目都有自己的编码样式指南。如果发生任何冲突,此类特定于项目的指南优先于该项目。

愚蠢的始终如一是小心灵中的妖精(Hobgoblin Of Little Minds)

Guido的一个重要见解是,读代码的次数比编写代码的次数多得多。这里提供的指导原则旨在提高代码的可读性,并使其在各种Python代码中保持一致。正如PEP 20所说,“可读性很重要”。

风格指南是关于一致性的。与本风格指南保持一致很重要。项目内部的一致性更重要。一个模块或功能内的一致性是最重要的。

然而,知道什么时候不一致–有时风格指南的建议就是不适用。当你有疑问的时候,用你最好的判断力。看看其他例子,决定哪一个看起来最好。别犹豫,尽管问吧!

特别要注意的是:不要仅仅为了遵守这个PEP而破坏向后兼容性!
忽略特定准则的其他一些很好的理由:

  1. 当应用该准则时,即使对于习惯于阅读遵循该PEP的代码的人来说,也会降低代码的可读性。
  2. 与周围破坏它的代码保持一致(可能是因为历史原因)–尽管这也是一个清理别人的烂摊子(真正的XP风格)的机会。
  3. 因为有问题的代码早于指南的引入,没有其他理由需要修改该代码。
  4. 当代码需要与不支持样式指南推荐的功能的旧版本的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个空格。这个PEP没有明确说明如何(或是否)在视觉上将这些条件行与if语句中的嵌套套件区分开来。在这种情况下,可接受的选项包括但不限于:

# 没有额外的缩进
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',
)

制表符还是空格?

空格是首选的缩进方法。

制表符只能用来保持与已经用制表符缩进的代码保持一致。

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

源文件编码

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

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

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

我们鼓励面向全球的开源项目采取类似的政策。

Imports

  • Imports 通常应在单独的行上:

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

    这种情况是可以的

    # 正确:
    from subprocess import Popen, PIPE
    
  • 导入(import)始终放在文件的顶部,紧跟在任何模块注释和文档字符串之后,模块全局变量和常量之前。

    应按以下顺序对导入(import)进行分组:

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

    在每组导入(import)之间放置一个空行

  • 建议使用绝对导入,因为如果导入系统配置不正确(例如,当程序包中的目录最终位于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.MyClass”和“foo.bar.youclass.YourClass”。

  • 应该避免通配符导入(从import*),因为它们使名称空间中存在哪些名称变得不清楚,从而使读者和许多自动化工具感到困惑。通配符导入有一个合理的用例,那就是将内部接口重新发布为公共API的一部分(例如,使用来自可选加速器模块的定义覆盖接口的纯Python实现,以及确切地说哪些定义将被覆盖事先是未知的)。

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

模块级 Dunder 名称

模块级别的“dunders”(即具有两个前导和两个尾随下划线的名称),例如 __all____author____version__等,应放在模块文档字符串之后,但在除__future__导入之外的任何导入语句之前。 Python 要求 future-imports 必须出现在模块中除文档字符串之外的任何其他代码之前:

"""这是示例模块。

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

from __future__ import barry_as_FLUFL

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

import os
import sys

字符串引号

在Python中,单引号字符串和双引号字符串是相同的。此PEP不对此提出建议。选择一条规则并坚持下去。但是,当字符串包含单引号或双引号字符时,请使用另一个引号字符,以避免字符串中出现反斜杠。它提高了可读性。
对于三重引号的字符串,请始终使用双引号字符,以符合PEP 257中的文档字符串约定。

表达式和语句中的空格

Pet Peeves

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

  • 紧接在圆括号、方括号或大括号内:

    # 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 : : upper]
    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 本身)具有拒绝它的预提交钩子。

  • 始终使用两侧的单个空格将这些二元运算符括起来:赋值(=)、扩充赋值(+=、-=等)、比较(==、<、>、!=、<>、<=、>=、in、not in、is、is、not)、布尔值(and或not)。

  • 如果使用不同优先级的运算符,请考虑在优先级最低的运算符周围添加空格。使用您自己的判断;但是,千万不要使用多个空格,并且二元运算符两侧的空格大小始终相同:

    # 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                 # 增量x

但有时,这很有用:

x = x + 1                 # 补偿边界

文档字符串

编写好的文档字符串(又名“文档字符串”)的约定在 PEP 257 中。

  • 为所有公共模块、函数、类和方法编写文档字符串。Docstring对于非公共方法不是必需的,但是您应该有一个注释来描述该方法的作用。此注释应出现在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

  • UPPERCASE

  • UPPER_CASE_WITH_UNDERSCORES

  • CapitalizedWords (或CapWords,或CamelCase–因其字母的凹凸不平的外观而得名).这有时也称为StudlyCaps。

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

  • mixedCase (与 CapitalizedWords 的不同之处在于初始小写字符!)

  • Capitalized_Words_With_Underscores (丑陋!)

还有一种使用简短的唯一前缀将相关名称组合在一起的风格。这在 Python 中使用不多,但为了完整性而提及。例如,os.stat() 函数返回一个元组,其项传统上具有 st_mode、st_size、st_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’(大写字母Eyes)作为单字符变量名称。

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

ASCII兼容性

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

包和模块名称

模块的名称应该是全小写的短名称。如果下划线提高了可读性,则可以在模块名称中使用下划线。虽然不鼓励使用下划线,但Python包也应该具有全小写的短名称。

当用C或C++编写的扩展模块具有提供更高级别(例如,更面向对象)接口的附带Python模块时,C/C++模块具有前导下划线(例如_Socket)。

类名

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

如果接口已记录在案,并且主要用作可调用的,则可以改用函数的命名约定。

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

类型变量名

PEP484中引入的类型变量的名称通常应该使用CapWord,而不是短名称:t、AnyStr、Num。建议在用于相应声明协变或逆变行为的变量中添加后缀_co或_conta:

```
from typing import TypeVar

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

异常名称

因为异常应该是类,所以类命名约定在这里适用。但是,您应该在异常名称上使用后缀“error”(如果异常实际上是一个错误)。

全局变量名

(我们希望这些变量只在一个模块中使用。)约定与函数的约定大致相同。

为通过 from M import * 使用而设计的模块应使用 __all__机制来防止导出全局变量,或使用在此类全局变量前加下划线的旧约定(您可能希望这样做以指示这些全局变量是“模块非公共”)。

函数和变量名

函数名称应为小写,并根据需要使用下划线分隔单词,以提高可读性。

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

为了保持向后兼容性,只允许在已经是主流样式(例如threading.py)的上下文中使用mixedCase。

函数和方法参数

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

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

如果函数参数的名称与保留关键字发生冲突,通常最好在结尾附加一个下划线,而不是使用缩写或拼写错误。因此 class_ 比 clss 好。 (也许更好的是通过使用同义词来避免这种冲突。)

方法名称和实例变量

根据需要使用函数命名规则:小写,单词之间用下划线分隔,以提高可读性。

只对非公共方法和实例变量使用一个前导下划线。

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

Python 将这些名称与类名相混淆:如果类 Foo 有一个名为 __a 的属性,则 Foo.__a 无法访问它。 (坚持使用的用户仍然可以通过调用 Foo._Foo__a 来获得访问权限。)通常,双前导下划线应该仅用于避免与设计为子类的类中的属性发生名称冲突。

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

常量

常量通常在模块级别定义,并且全部用大写字母书写,单词之间用下划线分隔。例如MAX_OVERFLOW和TOTAL。

为继承而设计

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

公共属性是您希望类中不相关的客户端使用的属性,并承诺避免向后不兼容的更改。非公共属性是那些不打算由第三方使用的属性;您不能保证非公共属性不会更改,甚至不会被删除。

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

另一类属性是属于“子类API”(在其他语言中通常称为“Protected”)的那些属性。有些类被设计为继承自,用于扩展或修改类行为的各个方面。在设计这样的类时,要注意明确决定哪些属性是公共的,哪些属性是子类API的一部分,哪些属性真正只供基类使用。

考虑到这一点,下面是Pythonic的指导方针:

  • 公共属性不应有前导下划线。

  • 如果您的公共属性名称与保留关键字冲突,请在您的属性名称后面附加一个尾随下划线。这比缩写或损坏的拼写更可取。(然而,尽管有这条规则,‘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+=b或a=a+b格式的语句提供的就地字符串连接的高效实现。即使在CPython中(它只对某些类型有效),这种优化也很脆弱,在不使用引用计数的实现中根本不存在。在库的性能敏感部分,应该改用".Join()形式。这将确保在各种实现中以线性时间进行串联。

  • 与 None 之类的单例的比较应该始终使用 is 或 not 来完成,而不是相等运算符。

    另外,当您真正的意思是如果x不是None时–例如,当测试缺省为None的变量或参数是否被设置为其他值时,请注意写入if x。另一个值的类型(如容器)在布尔上下文中可能为False!

  • 使用 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 > x 和 x < y,y >= x 和 x <= y,并且可以交换 x == y 和 x != y 的参数。 sort() 和 min() 操作保证使用 < 运算符,而 max() 函数使用 > 运算符。但是,最好实现所有六个操作,以免在其他情况下出现混淆。

  • 始终使用def语句,而不是将lambda表达式直接绑定到标识符的赋值语句:

    # Correct:
    def f(x): return 2*x
    
    # Wrong:
    f = lambda x: 2*x
    

    第一种形式意味着结果函数对象的名称是明确的‘f’,而不是泛型的‘’。一般来说,这对于回溯和字符串表示更有用。赋值语句的使用消除了lambda表达式相对于显式def语句所能提供的唯一好处(即它可以嵌入到更大的表达式中)。

  • 从异常派生异常,而不是从BaseException派生异常。从BaseException的直接继承是为异常保留的,在这种情况下捕获它们几乎总是错误的。

    基于捕获异常的代码可能需要来区别设计异常层次结构,而不是基于引发异常的位置。旨在以编程方式回答问题“哪里出了问题?”,而不是只说明“发生了问题”(请参阅PEP 3151以获取针对内置异常层次结构学到的本课示例)

    此处适用类命名约定,但如果异常是错误,则应在异常类中添加后缀“Error”。用于非本地流控制或其他形式的信令的非错误异常不需要特殊的后缀。

  • 适当使用异常链接。应使用“从Y提升X”来指示显式替换,而不会丢失原始回溯。

    故意替换内部异常时(使用 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:(Bare Except等效于Expect BaseException:)。

    一个好的经验法则是将裸 ‘except’ 子句的使用限制在两种情况下:

    1. 如果异常处理程序将打印出或记录回溯;至少用户会意识到发生了错误。
    2. 如果代码需要做一些清理工作,然后让异常通过 raise 向上传播。try...终于可以成为处理这种情况的更好方法。
  • 在捕获操作系统错误时,更喜欢 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 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...finallyfinally套件中使用流控制语句 return/break/continue,在这种情况下流控制语句会跳出finally套件。这是因为此类语句将隐式取消通过 finally套件传播的任何活动异常:

    # Wrong:
    def foo():
        try:
            1 / 0
        finally:
            return 42
    

函数注解

随着PEP 484的接受,函数注释的样式规则发生了变化。

  • 函数注解应该使用PEP 484语法(在上一节中有一些注解的格式建议)。

  • 不再鼓励对之前在此 PEP 中推荐的注释样式进行试验。

  • 但是,在标准库之外,现在鼓励在 PEP 484 规则内进行实验。例如,使用 PEP 484样式类型注释标记大型第三方库或应用程序,查看添加这些注释的难易程度,并观察它们的存在是否增加了代码的可理解性。

  • Python 标准库在采用此类注释时应该是保守的,但它们允许用于新代码和大型重构。

  • 对于想要以不同方式使用函数注释的代码,建议添加以下形式的注释:

    # type: ignore
    

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

  • 与 linter 一样,类型检查器是可选的、单独的工具。默认情况下,Python 解释器不应因类型检查而发出任何消息,也不应根据注释改变其行为。

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

变量注释

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
    
  • 尽管Python 3.6接受PEP 526 ,但变量注释语法是所有版本Python上存根文件的首选语法(有关详细信息,请参阅PEP 484)。

 类似资料: