添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
  • 注释里的 URL、路径名以及长的标志 (flag).

  • 不便于换行、不包含空格、模块级的长字符串常量, 比如 URL 或路径名.

  • Pylint 禁用注释. (例如: # pylint: disable=invalid-name )

  • 不要用反斜杠表示 显式续行 (explicit line continuation) .

    应该利用 Python 的 圆括号, 中括号和花括号的隐式续行 (implicit line joining) . 如有需要, 你可以在表达式外围添加一对括号.

    foo_bar(self, width, height, color='黑', design=None, x='foo',
            emphasis=None, highlight=0)
    if (width == 0 and height == 0 and
        color == '红' and emphasis == '加粗'):
    (bridge_questions.clarification_on
     .average_airspeed_of.unladen_swallow) = '美国的还是欧洲的?'
    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)
    
    if width == 0 and height == 0 and \
        color == '红' and emphasis == '加粗':
    bridge_questions.clarification_on \
        .average_airspeed_of.unladen_swallow = '美国的还是欧洲的?'
    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)
    

    如果字符串的字面量 (literal) 超过一行, 应该用圆括号实现隐式续行:

    x = ('这是一个很长很长很长很长很长很长'
         '很长很长很长很长很长的字符串')
    

    最好在最外层的语法结构上分行. 如果你需要多次换行, 应该在同一层语法结构上换行.

    bridgekeeper.answer(
         name="亚瑟", quest=questlib.find(owner="亚瑟", 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()
    
    bridgekeeper.answer(name="亚瑟", quest=questlib.find(
        owner="亚瑟", 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 可以独立成行.

    # 详情参见
    # http://www.example.com/us/developer/documentation/api/content/v2.0/csv_file_name_extension_full_specification.html
    
    # 详情参见
    # http://www.example.com/us/developer/documentation/api/content/\
    # v2.0/csv_file_name_extension_full_specification.html
    

    注意上面各个例子中的缩进; 详情参见 缩进 章节的解释.

    如果一行超过 80 个字符, 且 BlackPyink 自动格式化工具无法继续缩减行宽, 则允许该行超过 80 个字符. 我们也鼓励作者根据上面的规则手动拆分.

    使用括号时宁缺毋滥.

    可以把元组 (tuple) 括起来, 但不强制. 不要在返回语句或条件语句中使用括号, 除非用于隐式续行或表示元组.

    if foo:
        bar()
    while x:
        x = bar()
    if x and y:
        bar()
    if not x:
        bar()
    # 对于包含单个元素的元组, 括号比逗号更直观.
    onesie = (foo,)
    return foo
    return spam, beans
    return (spam, beans)
    for (x, y) in dict.items(): ...
    
    if (x):
        bar()
    if not(x):
        bar()
    return (foo)
    

    用4个空格作为缩进.

    不要使用制表符. 使用隐式续行时, 应该把括起来的元素垂直对齐(参见 行宽 章节的示例), 或者添加4个空格的悬挂缩进. 右括号 (圆括号, 方括号或花括号) 可以置于表达式结尾或者另起一行. 另起一行时右括号应该和左括号所在的那一行缩进相同.

    # 与左括号对齐.
    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,
    
    # 首行不能有元素.
    foo = long_function_name(var_one, var_two,
        var_three, var_four)
    # 禁止2个空格的悬挂缩进.
    foo = long_function_name(
      var_one, var_two, var_three,
      var_four)
    # 字典没有悬挂缩进.
    foo = {
        'long_dictionary_key':
        long_dictionary_value,
    

    大部分 .py 文件不必以 #! 开始. 可以根据 PEP-394 , 在程序的主文件开头添加 #!/usr/bin/env python3 (以支持 virtualenv) 或者 #!/usr/bin/python3.

    (译者注: 在计算机科学中, Shebang (也称为Hashbang)是一个由井号和叹号构成的字符串行(#!), 其出现在文本文件的第一行的前两个字符. 在文件中存在Shebang的情况下, 类Unix操作系统的程序载入器会分析Shebang后的内容, 将这些内容作为解释器指令, 并调用该指令, 并将载有Shebang的文件路径作为该解释器的参数. 例如, 以指令#!/bin/sh开头的文件在执行时会实际调用/bin/sh程序.)

    内核会通过这行内容找到Python解释器, 但是Python解释器在导入模块时会忽略这行内容. 这行内容仅对需要直接运行的文件有效.

    注释和文档字符串 (docstring)

    模块、函数、方法的文档字符串和内部注释一定要采用正确的风格.

    文档字符串

    Python 的文档字符串用于注释代码. 文档字符串是包、模块、类或函数里作为第一个语句的字符串. 可以用对象的 __doc__ 成员自动提取这些字符串, 并为 pydoc 所用. (可以试试在你的模块上运行 pydoc 并观察结果). 文档字符串一定要用三重双引号 """ 的格式 (依据 PEP-257 ). 文档字符串应该是一行概述 (整行不超过 80 个字符), 以句号、问号或感叹号结尾. 如果要写更多注释 (推荐), 那么概述后面必须紧接着一个空行, 然后是剩下的内容, 缩进与文档字符串的第一行第一个引号对齐. 下面是更多有关文档字符串的格式规范.

    每个文件应该包含一个许可协议模版. 应根据项目使用的许可协议 (例如, Apache 2.0, BSD, LGPL, GPL) 选择合适的模版.

    文件的开头应该是文档字符串, 其中应该描述该模块内容和用法.

    """模块或程序的一行概述, 以句号结尾.
    留一个空行. 接下来应该写模块或程序的总体描述. 也可以选择简要描述导出的类和函数,
    和/或描述使用示例.
    经典的使用示例:
    foo = ClassFoo()
    bar = foo.FunctionBar()
    

    测试文件不必包含模块级文档字符串. 只有在文档字符串可以提供额外信息时才需要写入文件.

    例如, 你可以描述运行测试时所需的特殊要求, 解释不常见的初始化模式, 描述外部环境的依赖等等.

    """这个blaze测试会使用样板文件(golden files).
    若要更新这些文件, 你可以在 `google3` 文件夹中运行
    `blaze run //foo/bar:foo_test -- --update_golden_files`
    

    不要使用不能提供额外信息的文档字符串.

    """foo.bar 的测试."""
    

    函数和方法

    本节中的函数是指函数、方法、生成器 (generator) 和特性 (property).

    满足下列任意特征的任何函数都必须有文档字符串:

  • 公开 API 的一部分

  • 逻辑不能一目了然

  • 文档字符串应该提供充分的信息, 让调用者无需阅读函数的代码就能调用函数. 文档字符串应该描述函数的调用语法和语义信息, 而不应该描述具体的实现细节, 除非这些细节会影响函数的用法. 比如, 如果函数的副作用是会修改某个传入的对象, 那就需要在文档字符串中说明. 对于微妙、重要但是与调用者无关的实现细节, 相较于在文档字符串里说明, 还是在代码中间加注释更好.

    文档字符串可以是陈述句 ("""Fetches rows from a Bigtable.""" ) 或者祈使句 ("""Fetch rows from a Bigtable."""), 不过一个文件内的风格应当一致. 对于 @property 修饰的数据描述符 (data descriptor), 文档字符串应采用和属性 (attribute) 或 函数参数 一样的风格 ("""Bigtable 路径.""" 而非 """返回 Bigtable 路径.""").

    对于覆写 (override) 基类 (base class) 方法的子类方法, 可以用简单的文档字符串引导读者阅读基类方法的文档字符串, 比如 """参见基类."""". 这样是为了避免到处复制基类方法中已有的文档字符串. 然而, 如果覆写的子类方法与基类方法截然不同, 或者有更多细节需要记录 (例如有额外的的副作用), 那么子类方法的文档字符串中至少要描述这些区别.

    函数的部分特征应该在以下列出特殊小节中记录. 每小节有一行标题, 标题以冒号结尾. 除标题行外, 小节的其他部分应有2个或4个空格 (同一文件内应保持一致) 的悬挂缩进. 如果函数名和函数签名 (signature) 可以见名知意, 以至于一行文档字符串就能恰当地描述该函数, 那么可以省略这些小节.

    Args: (参数:)

    列出所有参数名. 参数名后面是一个冒号, 然后是一个空格或者换行符, 最后是描述. 如果描述过长以至于一行超出了 80 字符, 则描述部分应该比参数名所在的行多2个或者4个空格 (文件内应当一致) 的悬挂缩进. 如果代码没有类型注解, 则描述中应该说明所需的类型. 如果一个函数有形如 *foo (可变长参数列表) 或者 **bar (任意关键字参数) 的参数, 那么列举参数名时应该写成 *foo**bar 的这样的格式.

    Returns: (“返回:”)

    生成器应该用 “Yields:” (“生成:” )

    描述返回值的类型和意义. 如果函数仅仅返回 None, 这一小节可以省略. 如果文档字符串以 Returns (返回) 或者 Yields (生成) 开头 (例如 """返回 Bigtable 的行, 类型是字符串构成的元组.""") 且这句话已经足以描述返回值, 也可以省略这一小节. 不要模仿 Numpy 风格的文档 (例子). 他们在文档中记录作为返回值的元组时, 写得就像返回值是多个值且每个值都有名字 (没有提到返回的是元组). 应该这样描述此类情况: “返回: 一个元组 (mat_a, mat_b), 其中 mat_a 是…, 且 …”. 文档字符串中使用的辅助名称不需要和函数体的内部变量名一致 (因为这些名称不是 API 的一部分).

    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, ...]]:
        """从 Smalltable 获取数据行.
        从 table_handle 代表的 Table 实例中检索指定键值对应的行. 如果键值是字符串,
        字符串将用 UTF-8 编码.
            table_handle: 处于打开状态的 smalltable.Table 实例.
            keys: 一个字符串序列, 代表要获取的行的键值. 字符串将用 UTF-8 编码.
            require_all_keys: 如果为 True, 只返回那些所有键值都有对应数据的
            一个字典, 把键值映射到行数据上. 行数据是字符串构成的元组. 例如:
            {b'Serak': ('Rigel VII', 'Preparer'),
             b'Zim': ('Irk', 'Invader'),
             b'Lrrr': ('Omicron Persei 8', 'Emperor')}
            返回的键值一定是字节串. 如果字典中没有 keys 参数中的某个键值, 说明
            表格中没有找到这一行 (且 require_all_keys 一定是 false).
            IOError: 访问 smalltable 时出现错误.
    

    以下这种在 Args (参数) 小节中换行的写法也是可以的:

    def fetch_smalltable_rows(
        table_handle: smalltable.Table,
        keys: Sequence[bytes | str],
        require_all_keys: bool = False,
    ) -> Mapping[bytes, tuple[str, ...]]:
        """从 Smalltable 获取数据行.
        从 table_handle 代表的 Table 实例中检索指定键值对应的行. 如果键值是字符串,
        字符串将用 UTF-8 编码.
            table_handle:
              处于打开状态的 smalltable.Table 实例.
            keys:
              一个字符串序列, 代表要获取的行的键值. 字符串将用 UTF-8 编码.
            require_all_keys:
              如果为 True, 只返回那些所有键值都有对应数据的行.
            一个字典, 把键值映射到行数据上. 行数据是字符串构成的元组. 例如:
            {b'Serak': ('Rigel VII', 'Preparer'),
             b'Zim': ('Irk', 'Invader'),
             b'Lrrr': ('Omicron Persei 8', 'Emperor')}
            返回的键值一定是字节串. 如果字典中没有 keys 参数中的某个键值, 说明
            表格中没有找到这一行 (且 require_all_keys 一定是 false).
            IOError: 访问 smalltable 时出现错误.
    

    类 (class)

    类的定义下方应该有一个描述该类的文档字符串. 如果你的类包含公有属性 (attributes), 应该在 Attributes (属性) 小节中记录这些属性, 格式与函数的 Args (参数) 小节类似.

    class SampleClass(object):
        """这里是类的概述.
        这里是更多信息....
        这里是更多信息....
            likes_spam: 布尔值, 表示我们是否喜欢午餐肉.
            eggs: 用整数记录的下蛋的数量.
        def __init__(self, likes_spam = False):
            """用某某某初始化 SampleClass."""
            self.likes_spam = likes_spam
            self.eggs = 0
        def public_method(self):
            """执行某某操作."""
    

    类的文档字符串开头应该是一行概述, 描述类的实例所代表的事物. 这意味着 Exception 的子类 (subclass) 应该描述这个异常代表什么, 而不是描述抛出异常时的环境. 类的文档字符串不应该有无意义的重复, 例如说这个类是一种类.

    class CheeseShopAddress:
    """奶酪店的地址.
    class OutOfCheeseError(Exception):
    """没有可用的奶酪."""
    
    class CheeseShopAddress:
    """一个描述奶酪店地址的类.
    class OutOfCheeseError(Exception):
    """在没有可用的奶酪时抛出."""
    

    块注释和行注释

    最后一种需要写注释的地方是代码中复杂的部分. 如果你可能在以后 代码评审 (code review) 时要解释某段代码, 那么现在就应该给这段代码加上注释. 应该在复杂的操作开始前写上若干行注释. 对于不是一目了然的代码, 应该在行尾添加注释.

    # 我们用加权的字典搜索, 寻找 i 在数组中的位置. 我们基于数组中的最大值和数组
    # 长度, 推断一个位置, 然后用二分搜索获得最终准确的结果.
    if i & (i-1) == 0:  # 如果 i 是 0 或者 2 的整数次幂, 则为真.
    

    为了提高可读性, 注释的井号和代码之间应有至少2个空格, 井号和注释之间应该至少有一个空格.

    除此之外, 绝不要仅仅描述代码. 应该假设读代码的人比你更懂Python, 只是不知道你的代码要做什么.

    # 不好的注释: 现在遍历数组 b, 确保每次 i 出现时, 下一个元素是 i+1
    

    注意标点符号、拼写和语法. 文笔好的注释比差的注释更容易理解.

    注释应该和记叙文一样可读, 使用恰当的大小写和标点. 一般而言, 完整的句子比残缺句更可读. 较短的注释 (比如行尾注释) 可以更随意, 但是你要保持风格一致.

    尽管你可能会因为代码审稿人指出你误把冒号写作逗号而灰心, 但是保持源代码清晰可读也是非常重要的. 正确的标点、拼写和语法有助于实现这一目标.

    字符串

    应该用 f-string% 运算符或 format 方法来格式化字符串. 即使所有参数都是字符串, 也如此. 你可以自行评判合适的选项. 可以用 + 实现单次拼接, 但是不要用 + 实现格式化.

    x = f'名称: {name}; 分数: {n}'
    x = '%s, %s!' % (imperative, expletive)
    x = '{}, {}'.format(first, second)
    x = '名称: %s; 分数: %d' % (name, n)
    x = '名称: %(name)s; 分数: %(score)d' % {'name':name, 'score':n}
    x = '名称: {}; 分数: {}'.format(name, n)
    x = a + b
    
    x = first + ', ' + second
    x = '名称: ' + name + '; 分数: ' + str(n)
    

    不要在循环中用 ++= 操作符来堆积字符串. 这有时会产生平方而不是线性的时间复杂度. 有时 CPython 会优化这种情况, 但这是一种实现细节. 我们无法轻易预测这种优化是否生效, 而且未来情况可能出现变化. 作为替代方案, 你可以将每个子串加入列表, 然后在循环结束后用 ''.join 拼接列表. 也可以将每个子串写入一个 io.StringIO 缓冲区中. 这些技巧保证始终有线性的平摊 (amortized) 时间复杂度.

    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)
    
    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>'
    

    应该保持同一文件中字符串引号的一致性. 选择 ' 或者 " 以后不要改变主意. 如果需要避免用反斜杠来转义引号, 则可以使用另一种引号.

    Python('为什么你要捂眼睛?')
    Gollum("I'm scared of lint errors. (我害怕格式错误.)")
    Narrator('"很好!" 一个开心的 Python 审稿人心想.')
    

    (译者注: 注意 “I’m” 中间有一个单引号,所以这一行的外层引号可以用不同的引号.)

    Python("为什么你要捂眼睛?")
    Gollum('格式检查器. 它在闪耀. 它要亮瞎我们.')
    Gollum("伟大的格式检查器永在. 它在看. 它在看.")
    

    多行字符串推荐使用 """ 而非 '''. 当且仅当项目中用 ' 给常规字符串打引号时, 才能在文档字符串以外的多行字符串上使用 '''. 无论如何, 文档字符串必须使用 """.

    多行字符串不会跟进代码其他部分的缩进. 如果需要避免字符串中的额外空格, 可以用多个单行字符串拼接, 或者用 textwrap.dedent() 删除每行开头的空格.

        long_string = """这样很难看.
    不要这样做.
    
    
    
    
        
    
    
    long_string = """如果你可以接受多余的空格,
        就可以这样."""
    long_string = ("如果你不能接受多余的空格,\n" +
                   "可以这样.")
    long_string = ("如果你不能接受多余的空格,\n"
                   "也可以这样.")
    
    import textwrap
    long_string = textwrap.dedent("""\
      这样也行, 因为 textwrap.dedent()
      会删除每一行开头共有的空格.""")
    

    注意, 这里的反斜杠没有违反 显式续行的禁令. 此时, 反斜杠用于在字符串字面量 (literal) 中 对换行符转义.

    对于那些第一个参数是格式字符串 (包含 % 占位符) 的日志函数: 一定要用字符串字面量 (而非 f-string!) 作为第一个参数, 并用占位符的参数作为其他参数. 有些日志的实现会收集未展开的格式字符串, 作为可搜索的项目. 这样也可以免于渲染那些被设置为不用输出的消息.

    import tensorflow as tf
    logger = tf.get_logger()
    logger.info('TensorFlow 的版本是: %s', tf.__version__)
    
    import os
    from absl import logging
    logging.info('当前的 $PAGER 是: %s', os.getenv('PAGER', default=''))
    homedir = os.getenv('HOME')
    if homedir is None or not os.access(homedir, os.W_OK):
        logging.error('无法写入主目录, $HOME=%r', homedir)
    
    import os
    from absl import logging
    logging.info('当前的 $PAGER 是:')
    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'无法写入主目录, $HOME={homedir!r}')
    

    错误信息 (例如: 诸如 ValueError 等异常的信息字符串和展示给用户的信息) 应该遵守以下三条规范:

  • 信息需要精确地匹配真正的错误条件.

  • 插入的片段一定要能清晰地分辨出来.

  • 要便于简单的自动化处理 (例如正则搜索, 也就是 grepping).

  • if not 0 <= p <= 1:
        raise ValueError(f'这不是概率值: {p!r}')
    try:
        os.rmdir(workdir)
    except OSError as error:
        logging.warning('无法删除这个文件夹 (原因: %r): %r',
                        error, workdir)
    
    if p < 0 or p > 1:  # 问题: 遇到 float('nan') 时也为假!
        raise ValueError(f'这不是概率值: {p!r}')
    try:
        os.rmdir(workdir)
    except OSError:
        # 问题: 信息中存在错误的揣测,
        # 删除操作可能因为其他原因而失败, 此时会误导调试人员.
        logging.warning('文件夹已被删除: %s', workdir)
    try:
        os.rmdir(workdir)
    except OSError:
        # 问题: 这个信息难以搜索, 而且某些 `workdir` 的值会让人困惑.
        # 假如有人调用这段代码时让 workdir = '已删除'. 这个警告会变成:
        # "无法删除已删除文件夹."
        logging.warning('无法删除%s文件夹.', workdir)
    

    使用完文件和套接字以后, 显式地关闭它们. 自然地, 这条规则也应该扩展到其他在内部使用套接字的可关闭资源 (比如数据库连接) 和其他需要用类似方法关停的资源. 其他例子还有 mmap 映射、 h5py 的文件对象matplotlib.pyplot 的图像窗口 .

    如果保持不必要的文件、套接字或其他有状态对象开启, 会产生很多缺点:

  • 它们可能消耗有限的系统资源, 例如文件描述符. 如果代码需要使用大量类似的资源而没有及时返还给系统, 就有可能出现原本可以避免的资源枯竭情况.

  • 保持文件的开启状态会阻碍其他操作, 例如移动、删除文件, 卸载 (unmont) 文件系统等等.

  • 如果程序的多个部分共享文件和套接字, 即使逻辑上文件已经关闭了, 仍然有可能出现意外的读写操作. 如果这些资源真正关闭了, 读写操作会抛出异常, 让问题早日浮出水面.

  • 此外, 即使文件和套接字 (以及其他行为类似的资源) 会在析构 (destruct) 时自动关闭, 把对象的生命周期和资源状态绑定的行为依然不妥:

  • 无法保证运行时 (runtime) 调用 __del__ 方法的真正时机. 不同的 Python 实现采用了不同的内存管理技巧 (比如延迟垃圾处理机制, delayed garbage collection), 可能会随意、无限期地延长对象的生命周期.

  • 意想不到的文件引用 (例如全局对象和异常的堆栈跟踪, exception tracebacks) 可能让文件的存续时间比想象的更长.

  • 依赖于终结器 (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
    

    少数情况下无法使用基于上下文 (context) 的资源管理, 此时文档应该清楚地解释代码会如何管理资源的生命周期.

    TODO (待办) 注释

    在临时、短期和不够完美的代码上添加 TODO (待办) 注释.

    待办注释以 TODO (待办) 这个全部大写的词开头, 紧跟着是用括号括起来的上下文标识符 (最好是 bug 链接, 有时是你的用户名). 最好是诸如 TODO(https://crbug.com/<bug编号>): 这样的 bug 链接, 因为 bug 有历史追踪和评论, 而程序员可能发生变动并忘记上下文. TODO 后面应该解释待办的事情.

    统一 TODO 的格式是为了方便搜索并查看详情. TODO 不代表注释中提到的人要做出修复问题的保证. 所以, 当你创建带有用户名的 TODO 时, 大部分情况下应该用你自己的用户名.

    # TODO(crbug.com/192795): 研究 cpufreq 的优化.
    # TODO(你的用户名): 提交一个议题 (issue), 用 '*' 代表重复.
    

    如果你的 TODO 形式类似于”将来做某事”, 请确保其中包含特别具体的日期 (“2009年11月前解决”) 或者特别具体的事件 (“当所有客户端都能处理 XML 响应时, 删除这些代码”), 以便于未来的代码维护者理解.

    导入 (import) 语句的格式

    导入语句应该各自独占一行. typing 和 collections.abc 的导入除外. 例如:

    from collections.abc import Mapping, Sequence
    import os
    import sys
    from typing import Any, NewType
    
    import os, sys
    

    导入语句必须在文件顶部, 位于模块的注释和文档字符串之后、全局变量和全局常量之前. 导入语句应该按照如下顺序分组, 从通用到特殊:

  • 导入 Python 的 __future__. 例如:

    from __future__ import annotations
    

    参见前文有关 __future__ 语句的描述.

  • 导入 Python 的标准库. 例如:

    import sys
    
  • 已废弃的规则: 导入应用专属的、与该文件属于同一个子包的模块. 例如:

    from myproject.backend.hgwells import time_machine
    

    你可能会在较老的谷歌风格 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
    # 旧的代码可能会把这些导入语句放在下面这里:
    #from myproject.backend.hgwells import time_machine
    #from myproject.backend.state_machine import main_loop
    

    通常每个语句应该独占一行.

    不过, 如果判断语句的主体与判断条件可以挤进一行, 你可以将它们放在同一行. 特别注意这不适用于 try / except, 因为 tryexcept 不能放在同一行. 只有在 if 语句没有对应的 else 时才适用.

    if foo: bar(foo)
    
    if foo: bar(foo)
    else:   baz(foo)
    try:               bar(foo)
    except ValueError: baz(foo)
    try:
        bar(foo)
    except ValueError: baz(foo)
    

    在访问和设置变量值时, 如果访问器和设置器 (又名为访问子 accessor 和变异子 mutator) 可以产生有意义的作用或效果, 则可以使用.

    特别来说, 如果在当下或者可以预见的未来, 读写某个变量的过程很复杂或者成本高昂, 则应该使用这种函数.

    如果一对访问器和设置器仅仅用于读写一个内部属性 (attribute), 你应该直接用公有属性取代它们. 相较而言, 如果设置操作会让部分状态无效化或引发重建, 则需要使用设置器. 显式的函数调用表示可能出现特殊的操作. 如果只有简单的逻辑, 或者在重构代码后不再需要访问器和设置器, 你可以用属性 (property) 替代.

    (译者注: 重视封装的面向对象程序员看到这个可能会很反感, 因为他们一直被教育: 所有成员变量都必须是私有的! 其实, 那真的是有点麻烦啊. 试着去接受Pythonic哲学吧)

    访问器和设置器应该遵守命名规范, 例如 get_foo()set_foo().

    如果之前的代码通过属性获取数据, 则不能把重新编写的访问器/设置器与这一属性绑定. 应该让任何用老办法访问变量的代码出现显眼的错误, 让使用者意识到代码复杂度有变化.

    模块名: module_name; 包名: package_name; 类名: ClassName; 方法名: method_name; 异常名: ExceptionName; 函数名: function_name, query_proper_noun_for_thing, send_acronym_via_https; 全局常量名: GLOBAL_CONSTANT_NAME ; 全局变量名: global_var_name; 实例名: instance_var_name; 函数参数名: function_parameter_name; 局部变量名: local_var_name.

    函数名、变量名和文件名应该是描述性的, 避免缩写. 特别要避免那些对于项目之外的人有歧义或不熟悉的缩写, 也不要通过省略单词中的字母来进行缩写.

    必须用 .py 作为文件后缀名. 不要用连字符.

    需要避免的名称

  • 只有单个字符的名称, 除了以下特别批准的情况:

  • 计数器和迭代器 (例如, i, j, k, v 等等).

  • try/except 语句中代表异常的 e.

  • with 语句中代表文件句柄的 f.

  • 私有的、没有约束 (constrain) 的类型变量 (type variable, 例如 _T = TypeVar("_T"), _P = ParamSpec("_P")).

  • 包含连字符(-) 的包名/模块名.

  • 首尾均为双下划线的名称, 例如 __double_leading_and_trailing_underscore__ (此类名称是 Python 的保留名称).

  • 包含冒犯性词语的名称.

  • 在不必要的情况下包含变量类型的名称 (例如 id_to_name_dict).

  • “内部(Internal)”一词表示仅在模块内可用, 或者在类内是受保护/私有的.

  • 在一定程度上, 在名称前加单下划线 (_) 可以保护模块变量和函数 (格式检查器会对受保护的成员访问操作发出警告).

  • 在实例的变量或方法名称前加双下划线 (__, 又名为 dunder) 可以有效地把变量或方法变成类的私有成员 (基于名称修饰 name mangling 机制). 我们不鼓励这种用法, 因为这会严重影响可读性和可测试性, 而且没有 真正 实现私有. 建议使用单下划线.

  • 应该把相关的类和顶级函数放在同一个模块里. 与Java不同, 不必限制一个模块只有一个类.

  • 类名应该使用首字母大写的形式 (如 CapWords), 但是模块名应该用小写加下划线的形式 (如 lower_with_under.py). 尽管有些旧的模块使用类似于 CapWords.py 这样的形式, 现在我们不再鼓励这种命名方式, 因为模块名和类名相同时会让人困惑 (“等等, 我刚刚写的是 import StringIO 还是 from StringIO import StringIO?”).

  • 新的 单元测试 文件应该遵守 PEP 8, 用小写加下划线格式的方法名, 例如 test_<被测试的方法名>_<状态>. 有些老旧的模块有形如 CapWords 这样大写的方法名, 为了保持风格一致, 可以在 test 这个词和方法名之后, 用下划线分割名称中不同的逻辑成分. 比如一种可行的格式之一是 test<被测试的方法>_<状态>.

  • 所有 Python 文件名都应该以 .py 为文件后缀且不能包含连字符 (-). 这样便于导入这些文件并编写单元测试. 如果想通过不含后缀的命令运行程序, 可以使用软链接文件 (symbolic link) 或者 exec "$0.py" "$@" 这样简单的 bash 脚本.

    根据Python之父Guido的建议所制定的规范

    对于涉及大量数学内容的代码, 如果相关论文或算法中有对应的符号, 则可以忽略以上命名规范并使用较短的变量名. 若要采用这种方法, 应该在注释或者文档字符串中注明你所使用的命名规范的来源. 如果原文无法访问, 则应该在文档中清楚地记录命名规范. 建议公开的 API 使用符合 PEP8 的、描述性的名称, 因为使用 API 的代码很可能缺少相关的上下文信息.

    主程序

    使用 Python 时, 提供给 pydoc 和单元测试的模块必须是可导入的. 如果一个文件是可执行文件, 该文件的主要功能应该位于 main() 函数中. 你的代码必须在执行主程序前检查 if __name__ == '__main__' , 这样导入模块时不会执行主程序.

    使用 absl 时, 请调用 app.run :

    from absl import app
    def main(argv):
        # 处理非标志 (non-flag) 参数
    if __name__ == '__main__':
        app.run(main)
    

    否则, 使用:

    def main():
    if __name__ == '__main__':
        main()
    

    导入模块时会执行该模块的所有顶级代码. 注意顶级代码中不能有 pydoc 不该执行的操作, 比如调用函数, 创建对象等.

    函数长度

    函数应该小巧且专一.

    我们承认有时长函数也是合理的, 所以不硬性限制函数长度. 若一个函数超过 40 行, 应该考虑在不破坏程序结构的前提下拆分这个函数.

    即使一个长函数现在没有问题, 几个月后可能会有别人添加新的效果. 此时容易出现隐蔽的错误. 保持函数简练, 这样便于别人阅读并修改你的代码.

    当你使用某些代码时, 可能发现一些冗长且复杂的函数. 要勇于修改现有的代码: 如果该函数难以使用或者存在难以调试的错误, 亦或是你想在不同场景下使用该函数的片段, 不妨考虑把函数拆分成更小、更容易管理的片段.

    类型注解 (type annotation)

  • 熟读 PEP-484 .

  • 仅在有额外类型信息时才需要注解方法中 selfcls 的类型. 例如:

    @classmethod
    def create(cls: Type[_T]) -> _T:
        return cls()
    

    尽量遵守前文所述的缩进规则.

    添加类型注解后, 很多函数签名 (signature) 会变成每行一个参数的形式. 若要让返回值单独成行, 可以在最后一个参数尾部添加逗号.

    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 对齐.

    def my_method(
        self,
        other_arg: MyLongType | None,
    ) -> tuple[MyLongType1, MyLongType1]:
    

    返回值类型和最后一个参数也可以放在同一行.

    可以接受:

    def my_method(
        self,
        first_var: int,
        second_var: int) -> dict[OtherLongType, MyLongType]:
    

    pylint 也允许你把右括号放在新行上, 与左括号对齐, 但相较而言可读性更差.

    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:
    

    若某个名称和对应的类型注解过长, 可以考虑用 别名 (alias) 代表类型. 下策是在冒号后换行并添加4格缩进.

    def my_function(
        long_variable_name:
            long_module_name.LongTypeName,
    ) -> None:
    
    def my_function(
        long_variable_name: long_module_name.
            LongTypeName,
    ) -> None:
    

    前向声明 (foward declaration)

    若需要使用一个尚未定义的类名 (比如想在声明一个类时使用自身的类名), 可以使用 from __future__ import annotations 或者字符串来代表类名.

    from __future__ import annotations
    class MyClass:
        def __init__(self, stack: Sequence[MyClass], item: OtherClass) -> None:
    class OtherClass:
    
  •