diff --git a/google-python-styleguide/index.rst b/google-python-styleguide/index.rst index 5cc6097..dfbccca 100644 --- a/google-python-styleguide/index.rst +++ b/google-python-styleguide/index.rst @@ -19,11 +19,11 @@ `guoqiao `_ v2.19 `xuxinkun `_ v2.59 `captainfffsama `_ v2.6 - `楼宇 `_ 2023 年 4 月 5 日更新 + `楼宇 `_ 2023 年 4 月 16 日更新 :项目主页: - `Google Style Guide (英文原版) `_ - `Google 开源项目风格指南 - 中文版 `_ :协议: - Python风格指南的源代码采用Apache License 2.0协议, 文本内容采用 `CC-BY 3.0 `_ 协议. + Python风格指南的源文件采用Apache License 2.0协议, 文本内容采用 `CC-BY 3.0 `_ 协议. diff --git a/google-python-styleguide/parting_words.rst b/google-python-styleguide/parting_words.rst index 502d40d..4af3e4f 100644 --- a/google-python-styleguide/parting_words.rst +++ b/google-python-styleguide/parting_words.rst @@ -1,23 +1,8 @@ 临别赠言 ================================ -**请务必保持代码的一致性** +**务必保持一致性.** -如果你正在编辑代码, 花几分钟看一下周边代码, 然后决定风格. 如果它们在所有的算术操作符两边都使用空格, 那么你也应该这样做. 如果它们的注释都用标记包围起来, 那么你的注释也要这样. +编辑代码时, 请花几分钟观察一下周边代码的风格. 如果这些代码在所有运算符的周围加上了空格, 那么你也应该这样做. 如果这些代码的注释都用井号形成的框包围起来, 那么你的注释也要用井号形成的框包起来. -制定风格指南的目的在于让代码有规可循, 这样人们就可以专注于"你在说什么", 而不是"你在怎么说". 我们在这里给出的是全局的规范, 但是本地的规范同样重要. 如果你加到一个文件里的代码和原有代码大相径庭, 它会让读者不知所措. 避免这种情况. - - - -.. line-block:: - - Revision 2.60 - - Amit Patel - Antoine Picard - Eugene Jhong - Gregory P. Smith - Jeremy Hylton - Matt Smart - Mike Shields - Shane Liebling +制定风格指南是为了像字典一样让代码有章可循. 这样人们可以专注于"写什么", 而不是纠结"怎么写". 我们在这里列出的全局规范就像字典, 但是局部的规范同样重要. 如果你添加的代码和周围原有的代码大相径庭, 就会打乱读者的阅读节奏. 不要这样. diff --git a/google-python-styleguide/python_language_rules.rst b/google-python-styleguide/python_language_rules.rst index f2504bf..073f3ba 100644 --- a/google-python-styleguide/python_language_rules.rst +++ b/google-python-styleguide/python_language_rules.rst @@ -5,7 +5,7 @@ Lint -------------------- .. tip:: - 用 `pylintrc `_ 运行 pylint,以检查你的代码. + 用 `pylintrc `_ 运行 pylint, 以检查你的代码. 定义: pylint 是在 Python 代码中寻找 bug 和格式问题的工具. 它寻找的问题就像 C 和 C++ 这些更静态的(译者注: 原文是less dynamic)语言中编译器捕捉的问题. 出于Python的动态特性, 部分警告可能有误. 不过, 误报应该不常见. @@ -43,6 +43,7 @@ Lint def viking_cafe_order(spam: str, beans: str, eggs: str | None = None) -> str: del beans, eggs # 未被维京人使用. return spam + spam + spam + (译者注:Viking 意为维京人.) 其他避免这种警告的常用方法还有: 用`_`作为未使用参数的名称; 给这些参数名加上前缀 ``unused_``; 或者把它们赋值给变量 ``_``. 我们允许但是不再推荐这些方法. 这会导致调用者无法通过参数名来传参,也不能保证变量确实没被引用。 @@ -497,35 +498,35 @@ Lambda函数 ... -特性(properties) +特性 (properties) -------------------- (译者注:参照fluent python.这里将 "property" 译为"特性",而 "attribute" 译为属性. python中数据的属性和处理数据的方法统称属性"(arrtibute)", 而在不改变类接口的前提下用来修改数据属性的存取方法我们称为"特性(property)".) .. tip:: - 可以用特性来读取、写入涉及简单计算、逻辑的属性. 特性的实现必须和属性一样满足这些通用要求: 轻量、直白、明确. + 可以用特性来读取或设置涉及简单计算、逻辑的属性. 特性的实现必须和属性 (attribute) 一样满足这些通用要求: 轻量、直白、明确. 定义: - 把读取、写入属性的函数包装为常规属性操作的写法. + 把读取、设置属性的函数包装为常规属性操作的写法. 优点: - #. 可以直接实现属性的访问、赋值接口, 而不必添加访问函数 (getter) 和变异函数 (setter). + #. 可以直接实现属性的访问、赋值接口, 而不必添加获取器 (getter) 和设置器 (setter). #. 可以让属性变为只读. #. 可以实现惰性求值. #. 类的内部实现发生变化时, 可以用这种方法让用户看到的公开接口保持不变. 缺点: - #. 可能掩盖副作用, 类似运算符重载. + #. 可能掩盖副作用, 类似运算符重载 (operator overload). #. 子类继承时可能产生困惑. 结论: - 允许使用特性, 但是, 和运算符重载一样, 只能在必要时使用, 并且要模仿常规属性的存取特点. 若无法满足要求, 请参考访问函数和变异函数的规则. + 允许使用特性. 但是, 和运算符重载一样, 只能在必要时使用, 并且要模仿常规属性的存取特点. 若无法满足要求, 请参考 :ref:`设置器和写入器 ` 的规则. 举个例子, 一个特性不能仅仅用于获取和设置一个内部属性: 因为不涉及计算, 没有必要用特性 (应该把该属性设为公有). 而用特性来限制属性的访问或者计算 **简单** 的衍生值则是正确的: 这种逻辑简单明了. - 应该用 ``@property`` `装饰器 `_ 来创建特性. 自行实现的特性装饰器属于威力过大的功能. + 应该用 ``@property`` `装饰器 (decorator) `_ 来创建特性. 自行实现的特性装饰器属于威力过大的功能. - 特性的继承机制难以理解. 不要用特性实现子类能覆盖或扩展的计算功能. + 特性的继承机制难以理解. 不要用特性实现子类能覆写 (override) 或扩展的计算功能. True/False的求值 -------------------- @@ -581,7 +582,7 @@ True/False的求值 #. 注意, Numpy 数组转换为布尔值时可能抛出异常. 因此建议用 `.size` 属性检查 ``np.array`` 是否为空 (例如 ``if not users.size``). 词法作用域(Lexical Scoping, 又名静态作用域) ------------------------------ +--------------------------------------------- .. tip:: 可以使用. @@ -701,7 +702,7 @@ True/False的求值 现代python: from __future__ imports --------------------- +-------------------------------------- .. tip:: 可以通过导入 ``__future__`` 包, 在较老的运行时上启用新语法, 并且只在特定文件上生效. diff --git a/google-python-styleguide/python_style_rules.rst b/google-python-styleguide/python_style_rules.rst index 248daa1..a6b2383 100644 --- a/google-python-styleguide/python_style_rules.rst +++ b/google-python-styleguide/python_style_rules.rst @@ -5,114 +5,159 @@ Python风格规范 -------------------- .. tip:: - 不要在行尾加分号, 也不要用分号将两条命令放在同一行. + 不要在行尾加分号, 也不要用分号将两条语句合并到一行. .. _line_length: -行长度 +行宽 -------------------- .. tip:: - 每行不超过80个字符 + 最大行宽是 80 个字符. 例外: -#. 长的导入模块语句 -#. 注释里的URL,路径以及其他的一些长标记 -#. 不便于换行,不包含空格的模块级字符串常量,比如url或者路径 - - #. Pylint 禁用注释.(例如:``# pylint: disable=invalid-name) +#. 长的导入 (import) 语句. +#. 注释里的 URL、路径名以及长的标志 (flag). +#. 不便于换行、不包含空格、模块级的长字符串常量, 比如 URL 或路径名. +#. Pylint 禁用注释. (例如: ``# pylint: disable=invalid-name``) -除非是在 ``with`` 语句需要三个以上的上下文管理器的情况下,否则不要使用反斜杠连接行. +不要用反斜杠表示 `显式续行 (explicit line continuation) `_. -Python会将 `圆括号, 中括号和花括号中的行隐式的连接起来 `_ , 你可以利用这个特点. 如果需要, 你可以在表达式外围增加一对额外的圆括号. +应该利用 Python 的 `圆括号, 中括号和花括号的隐式续行 (implicit line joining) `_ . 如有需要, 你可以在表达式外围添加一对括号. + +正确: .. code-block:: python - Yes: foo_bar(self, width, height, color='black', design=None, x='foo', - emphasis=None, highlight=0) + foo_bar(self, width, height, color='黑', design=None, x='foo', + emphasis=None, highlight=0) - if (width == 0 and height == 0 and - color == 'red' and emphasis == 'strong'): + 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) + +错误: .. code-block:: python - x = ('This will build a very long long ' - 'long long long long long long string') + if width == 0 and height == 0 and \ + color == '红' and emphasis == '加粗': -在注释中,如果必要,将长的URL放在一行上。 + 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) 超过一行, 应该用圆括号实现隐式续行: .. code-block:: python - Yes: # See details at - # http://www.example.com/us/developer/documentation/api/content/v2.0/csv_file_name_extension_full_specification.html + x = ('这是一个很长很长很长很长很长很长' + '很长很长很长很长很长的字符串') -.. code-block:: python - - No: # See details at - # http://www.example.com/us/developer/documentation/api/content/\ - # v2.0/csv_file_name_extension_full_specification.html +最好在最外层的语法结构上分行. 如果你需要多次换行, 应该在同一层语法结构上换行. -当 ``with`` 表达式需要使用三个及其以上的上下文管理器时,可以使用反斜杠换行.若只需要两个,请使用嵌套的with. +正确: .. code-block:: python - Yes: 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) + 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() + +错误: .. code-block:: python - No: with VeryLongFirstExpressionFunction() as spam, \ - VeryLongSecondExpressionFunction() as beans: - PlaceOrder(eggs, beans, spam, beans) + 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 可以独立成行. + +正确: .. code-block:: python - Yes: with very_long_first_expression_function() as spam: - with very_long_second_expression_function() as beans: - place_order(beans, spam) + # 详情参见 + # http://www.example.com/us/developer/documentation/api/content/v2.0/csv_file_name_extension_full_specification.html -注意上面例子中的元素缩进; 你可以在本文的 :ref:`缩进 ` 部分找到解释. +错误: -另外在其他所有情况下,若一行超过80个字符,但 `yapf `_ 却无法将该行字数降至80个字符以下时,则允许该行超过80个字符长度. +.. code-block:: python + # 详情参见 + # http://www.example.com/us/developer/documentation/api/content/\ + # v2.0/csv_file_name_extension_full_specification.html + +注意上面各个例子中的缩进; 详情参见 :ref:`缩进 ` 章节的解释. + +如果一行超过 80 个字符, 且 `Black `_ 或 `Pyink `_ 自动格式化工具无法继续缩减行宽, 则允许该行超过 80 个字符. 我们也鼓励作者根据上面的规则手动拆分. 括号 -------------------- .. tip:: - 宁缺毋滥的使用括号 + 使用括号时宁缺毋滥. + +可以把元组 (tuple) 括起来, 但不强制. 不要在返回语句或条件语句中使用括号, 除非用于隐式续行或表示元组. + +正确: -除非是用于实现行连接, 否则不要在返回语句或条件语句中使用括号. 不过在元组两边使用括号是可以的. - .. code-block:: python - Yes: if foo: - bar() - while x: - x = bar() - if x and y: - bar() - if not x: - bar() - # For a 1 item tuple the ()s are more visually obvious than the comma. - onesie = (foo,) - return foo - return spam, beans - return (spam, beans) - for (x, y) in dict.items(): ... - + 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(): ... + +错误: + .. code-block:: python - No: if (x): - bar() - if not(x): - bar() - return (foo) + if (x): + bar() + if not(x): + bar() + return (foo) .. _indentation: @@ -120,528 +165,522 @@ Python会将 `圆括号, 中括号和花括号中的行隐式的连接起来 ` 部分的示例), 或者使用4空格的悬挂式缩进(这时第一行不应该有参数): - +不要使用制表符. 使用隐式续行时, 应该把括起来的元素垂直对齐(参见 :ref:`行宽 ` 章节的示例), 或者添加4个空格的悬挂缩进. 右括号 (圆括号, 方括号或花括号) 可以置于表达式结尾或者另起一行. 另起一行时右括号应该和左括号所在的那一行缩进相同. + +正确: + .. code-block:: python - Yes: # Aligned with opening delimiter - foo = long_function_name(var_one, var_two, - var_three, var_four) - - # Aligned with opening delimiter in a dictionary - foo = { - long_dictionary_key: value1 + - value2, - ... - } - - # 4-space hanging indent; nothing on first line - foo = long_function_name( - var_one, var_two, var_three, - var_four) - - # 4-space hanging indent in a dictionary - foo = { - long_dictionary_key: - long_dictionary_value, - ... - } - + # 与左括号对齐. + 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, + ... + } + +错误: + .. code-block:: python - No: # Stuff on first line forbidden - foo = long_function_name(var_one, var_two, - var_three, var_four) - - # 2-space hanging indent forbidden - foo = long_function_name( - var_one, var_two, var_three, - var_four) - - # No hanging indent in a dictionary - 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, + ... + } -序列元素尾部逗号 +序列的尾部要添加逗号吗? +----------------------- + +.. tip:: + 仅当 ``]``, ``)``, ``}`` 和最后一个元素不在同一行时, 推荐在序列尾部添加逗号. 我们的 Python 自动格式化工具会把尾部的逗号视为一种格式提示. + +Shebang行 -------------------- .. tip:: - 仅当 ``]``, ``)``, ``}`` 和末位元素不在同一行时,推荐使用序列元素尾部逗号. 当末位元素尾部有逗号时,元素后的逗号可以指示 `YAPF `_ 将序列格式化为每行一项. - -.. code-block:: python - - Yes: golomb3 = [0, 1, 3] - Yes: golomb4 = [ - 0, - 1, - 4, - 6, - ] - -.. code-block:: python - - No: golomb4 = [ - 0, - 1, - 4, - 6 - ] - -空行 --------------------- - -.. tip:: - 顶级定义之间空两行, 方法定义之间空一行 - -顶级定义之间空两行, 比如函数或者类定义. 方法定义, 类定义与第一个方法之间, 都应该空一行. 函数或方法中, 某些地方要是你觉得合适, 就空一行. - - -空格 --------------------- - -.. tip:: - 按照标准的排版规范来使用标点两边的空格 - -括号内不要有空格. - -.. code-block:: python - - Yes: spam(ham[1], {eggs: 2}, []) - -.. code-block:: python - - No: spam( ham[ 1 ], { eggs: 2 }, [ ] ) - -不要在逗号, 分号, 冒号前面加空格, 但应该在它们后面加(除了在行尾). - -.. code-block:: python - - Yes: if x == 4: - print(x, y) - x, y = y, x - -.. code-block:: python - - No: if x == 4 : - print(x , y) - x , y = y , x - -参数列表, 索引或切片的左括号前不应加空格. - -.. code-block:: python - - Yes: spam(1) - -.. code-block:: python - - no: spam (1) - -.. code-block:: python - - Yes: dict['key'] = list[index] - -.. code-block:: python - - No: dict ['key'] = list [index] - -在二元操作符两边都加上一个空格, 比如赋值(=), 比较(==, <, >, !=, <>, <=, >=, in, not in, is, is not), 布尔(and, or, not). 至于算术操作符两边的空格该如何使用, 需要你自己好好判断. 不过两侧务必要保持一致. - -.. code-block:: python - - Yes: x == 1 - -.. code-block:: python - - No: x<1 - -当 ``=`` 用于指示关键字参数或默认参数值时, 不要在其两侧使用空格. 但若存在类型注释的时候,需要在 ``=`` 周围使用空格. - -.. code-block:: python - - Yes: def complex(real, imag=0.0): return magic(r=real, i=imag) - Yes: def complex(real, imag: float = 0.0): return Magic(r=real, i=imag) - - -.. code-block:: python - - No: def complex(real, imag = 0.0): return magic(r = real, i = imag) - No: def complex(real, imag: float=0.0): return Magic(r = real, i = imag) - -不要用空格来垂直对齐多行间的标记, 因为这会成为维护的负担(适用于:, #, =等): - -.. code-block:: python - - Yes: - foo = 1000 # comment - long_name = 2 # comment that should not be aligned - - dictionary = { - "foo": 1, - "long_name": 2, - } - -.. code-block:: python - - No: - foo = 1000 # comment - long_name = 2 # comment that should not be aligned - - dictionary = { - "foo" : 1, - "long_name": 2, - } - -Shebang --------------------- - -.. tip:: - 大部分.py文件不必以#!作为文件的开始. 根据 `PEP-394 `_ , 程序的main文件应该以 ``#!/usr/bin/python2`` 或者 ``#!/usr/bin/python3`` 开始. + 大部分 ``.py`` 文件不必以 ``#!`` 开始. 可以根据 `PEP-394 `_ , 在程序的主文件开头添加 ``#!/usr/bin/env python3`` (以支持 virtualenv) 或者 ``#!/usr/bin/python3``. (译者注: 在计算机科学中, `Shebang `_ (也称为Hashbang)是一个由井号和叹号构成的字符串行(#!), 其出现在文本文件的第一行的前两个字符. 在文件中存在Shebang的情况下, 类Unix操作系统的程序载入器会分析Shebang后的内容, 将这些内容作为解释器指令, 并调用该指令, 并将载有Shebang的文件路径作为该解释器的参数. 例如, 以指令#!/bin/sh开头的文件在执行时会实际调用/bin/sh程序.) -``#!`` 先用于帮助内核找到Python解释器, 但是在导入模块时, 将会被忽略. 因此只有被直接执行的文件中才有必要加入 ``#!`` . - - +内核会通过这行内容找到Python解释器, 但是Python解释器在导入模块时会忽略这行内容. 这行内容仅对需要直接运行的文件有效. + .. _comments: -注释 --------------------- +注释和文档字符串 (docstring) +---------------------------- .. tip:: - 确保对模块, 函数, 方法和行内注释使用正确的风格 + 模块、函数、方法的文档字符串和内部注释一定要采用正确的风格. **文档字符串** - Python有一种独一无二的的注释方式: 使用文档字符串. 文档字符串是包, 模块, 类或函数里的第一个语句. 这些字符串可以通过对象的 ``__doc__`` 成员被自动提取, 并且被pydoc所用. (你可以在你的模块上运行pydoc试一把, 看看它长什么样). 我们对文档字符串的惯例是使用三重双引号"""( `PEP-257 `_ ). 一个文档字符串应该这样组织: 首先是一行以句号, 问号或惊叹号结尾的概述(或者该文档字符串单纯只有一行). 接着是一个空行. 接着是文档字符串剩下的部分, 它应该与文档字符串的第一行的第一个引号对齐. 下面有更多文档字符串的格式化规范. - + Python 的文档字符串用于注释代码. 文档字符串是位于包、模块、类或函数里第一个语句的字符串. 可以用对象的 ``__doc__`` 成员自动提取这些字符串, 并为 ``pydoc`` 所用. (可以试试在你的模块上运行 ``pydoc`` 并观察结果). 文档字符串一定要用三重双引号 ``"""`` 的格式 (依据 `PEP-257 `_ ). 文档字符串应该是一行概述 (整行不超过 80 个字符), 以句号、问号或感叹号结尾. 如果要写更多注释 (推荐), 那么概述后面必须紧接着一个空行, 然后是剩下的内容, 缩进与文档字符串的第一行的第一个引号对齐. 下面是更多有关文档字符串的格式规范. + **模块** - 每个文件应该包含一个许可样板. 根据项目使用的许可(例如, Apache 2.0, BSD, LGPL, GPL), 选择合适的样板. - 其开头应是对模块内容和用法的描述. + 每个文件应该包含一个许可协议模版. 应根据项目使用的许可协议 (例如, Apache 2.0, BSD, LGPL, GPL) 选择合适的模版. -.. code-block:: python + 文件的开头应该是文档字符串, 其中应该描述该模块内容和用法. - """A one line summary of the module or program, terminated by a period. + .. code-block:: python - Leave one blank line. The rest of this docstring should contain an - overall description of the module or program. Optionally, it may also - contain a brief description of exported classes and functions and/or usage - examples. + """模块或程序的一行概述, 以句号结尾. - Typical usage example: + 留一个空行. 接下来应该写模块或程序的总体描述. 也可以选择简要描述导出的类和函数, + 和/或描述使用示例. - foo = ClassFoo() - bar = foo.FunctionBar() - """ + 经典的使用示例: + + foo = ClassFoo() + bar = foo.FunctionBar() + """ + +**测试模块** + + 测试文件不必包含模块级文档字符串. 只有在文档字符串可以提供额外信息时才需要写入文件. + + 例如, 你可以描述运行测试时所需的特殊要求, 解释不常见的初始化模式, 描述外部环境的依赖等等. + + .. code-block:: python + + """这个blaze测试会使用样板文件. + + 若要更新这些文件, 你可以在 `google3` 文件夹中运行 + `blaze run //foo/bar:foo_test -- --update_golden_files` + """ + + 不要使用不能提供额外信息的文档字符串. + + .. code-block:: python + + """foo.bar 的测试.""" **函数和方法** - 下文所指的函数,包括函数, 方法, 以及生成器. - - 一个函数必须要有文档字符串, 除非它满足以下条件: - - #. 外部不可见 - #. 非常短小 - #. 简单明了 - - 文档字符串应该包含函数做什么, 以及输入和输出的详细描述. 通常, 不应该描述"怎么做", 除非是一些复杂的算法. 文档字符串应该提供足够的信息, 当别人编写代码调用该函数时, 他不需要看一行代码, 只要看文档字符串就可以了. 对于复杂的代码, 在代码旁边加注释会比使用文档字符串更有意义. - 覆盖基类的子类方法应有一个类似 ``See base class`` 的简单注释来指引读者到基类方法的文档注释.若重载的子类方法和基类方法有很大不同,那么注释中应该指明这些信息. - - 关于函数的几个方面应该在特定的小节中进行描述记录, 这几个方面如下文所述. 每节应该以一个标题行开始. 标题行以冒号结尾. 除标题行外, 节的其他内容应被缩进2个空格. - - Args: - 列出每个参数的名字, 并在名字后使用一个冒号和一个空格, 分隔对该参数的描述.如果描述太长超过了单行80字符,使用2或者4个空格的悬挂缩进(与文件其他部分保持一致). - 描述应该包括所需的类型和含义. - 如果一个函数接受*foo(可变长度参数列表)或者**bar (任意关键字参数), 应该详细列出*foo和**bar. + 本节中的函数是指函数、方法、生成器 (generator) 和特性 (property). - Returns: (或者 Yields: 用于生成器) - 描述返回值的类型和语义. 如果函数返回None, 这一部分可以省略. + 满足下列任意特征的任何函数都必须有文档字符串: - Raises: - 列出与接口有关的所有异常. + #. 公开 API 的一部分 + #. 长度过长 + #. 逻辑不能一目了然 + + 文档字符串应该提供充分的信息, 让调用者无需阅读函数的代码就能调用函数. 文档字符串应该描述函数的调用语法和语义信息, 而不应该描述具体的实现细节, 除非这些细节会影响函数的用法. 比如, 如果函数的副作用是会修改某个传入的对象, 那就需要在文档字符串中说明. 对于微妙、重要但是与调用者无关的实现细节, 相较于在文档字符串里说明, 还是在代码中间加注释更好. + + 文档字符串可以是陈述句 (``"""Fetches rows from a Bigtable."""``) 或者祈使句 (``"""Fetch rows from a Bigtable."""``), 不过一个文件内的风格应当一致. 对于 ``@property`` 修饰的数据描述符 (data descriptor), 文档字符串应采用和属性 (attribute) 或 :ref:`函数参数 ` 一样的风格 (``"""Bigtable 路径."""`` 而非 ``"""返回 Bigtable 路径."""``). + + 对于覆写 (override) 基类 (base class) 方法的子类方法, 可以用简单的文档字符串引导读者阅读基类方法的文档字符串, 比如 ``"""参见基类.""""``. 这样是为了避免到处复制基类方法中已有的文档字符串. 然而, 如果覆写的子类方法与基类方法截然不同, 或者有更多细节需要记录 (例如有额外的的副作用), 那么子类方法的文档字符串中至少要描述这些区别. + + 函数的部分特征应该在以下列出特殊小节中记录. 每小节有一行标题, 标题以冒号结尾. 除标题行外, 小节的其他部分应有2个或4个空格 (同一文件内应保持一致) 的悬挂缩进. 如果函数名和函数签名 (signature) 可以见名知意, 以至于一行文档字符串就能恰当地描述该函数, 那么可以省略这些小节. + +.. _doc_function_args: + + 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 的一部分, 这是矛盾的). .. code-block:: python - def fetch_smalltable_rows(table_handle: smalltable.Table, - keys: Sequence[Union[bytes, str]], - require_all_keys: bool = False, - ) -> Mapping[bytes, Tuple[str]]: - """Fetches rows from a Smalltable. + def fetch_smalltable_rows( + table_handle: smalltable.Table, + keys: Sequence[bytes | str], + require_all_keys: bool = False, + ) -> Mapping[bytes, tuple[str, ...]]: + """从 Smalltable 获取数据行. - Retrieves rows pertaining to the given keys from the Table instance - represented by table_handle. String keys will be UTF-8 encoded. + 从 table_handle 代表的 Table 实例中检索指定键值对应的行. 如果键值是字符串, + 字符串将用 UTF-8 编码. - Args: - table_handle: An open smalltable.Table instance. - keys: A sequence of strings representing the key of each table - row to fetch. String keys will be UTF-8 encoded. - require_all_keys: Optional; If require_all_keys is True only - rows with values set for all keys will be returned. + 参数: + table_handle: 处于打开状态的 smalltable.Table 实例. + keys: 一个字符串序列, 代表要获取的行的键值. 字符串将用 UTF-8 编码. + require_all_keys: 如果为 True, 只返回那些所有键值都有对应数据的 + 行. - Returns: - A dict mapping keys to the corresponding table row data - fetched. Each row is represented as a tuple of strings. For - example: + 返回: + 一个字典, 把键值映射到行数据上. 行数据是字符串构成的元组. 例如: {b'Serak': ('Rigel VII', 'Preparer'), - b'Zim': ('Irk', 'Invader'), - b'Lrrr': ('Omicron Persei 8', 'Emperor')} + b'Zim': ('Irk', 'Invader'), + b'Lrrr': ('Omicron Persei 8', 'Emperor')} - Returned keys are always bytes. If a key from the keys argument is - missing from the dictionary, then that row was not found in the - table (and require_all_keys must have been False). + 返回的键值一定是字节串. 如果字典中没有 keys 参数中的某个键值, 说明 + 表格中没有找到这一行 (且 require_all_keys 一定是 false). - Raises: - IOError: An error occurred accessing the smalltable. + 抛出: + IOError: 访问 smalltable 时出现错误. """ - 在 ``Args:`` 上进行换行也是可以的: + 以下这种在 Args (参数) 小节中换行的写法也是可以的: .. code-block:: python - def fetch_smalltable_rows(table_handle: smalltable.Table, - keys: Sequence[Union[bytes, str]], - require_all_keys: bool = False, - ) -> Mapping[bytes, Tuple[str]]: - """Fetches rows from a Smalltable. + def fetch_smalltable_rows( + table_handle: smalltable.Table, + keys: Sequence[bytes | str], + require_all_keys: bool = False, + ) -> Mapping[bytes, tuple[str, ...]]: + """从 Smalltable 获取数据行. - Retrieves rows pertaining to the given keys from the Table instance - represented by table_handle. String keys will be UTF-8 encoded. + 从 table_handle 代表的 Table 实例中检索指定键值对应的行. 如果键值是字符串, + 字符串将用 UTF-8 编码. - Args: - table_handle: - An open smalltable.Table instance. - keys: - A sequence of strings representing the key of each table row to - fetch. String keys will be UTF-8 encoded. - require_all_keys: - Optional; If require_all_keys is True only rows with values set - for all keys will be returned. + 参数: + table_handle: + 处于打开状态的 smalltable.Table 实例. + keys: + 一个字符串序列, 代表要获取的行的键值. 字符串将用 UTF-8 编码. + require_all_keys: + 如果为 True, 只返回那些所有键值都有对应数据的行. - Returns: - A dict mapping keys to the corresponding table row data - fetched. Each row is represented as a tuple of strings. For - example: + 返回: + 一个字典, 把键值映射到行数据上. 行数据是字符串构成的元组. 例如: - {b'Serak': ('Rigel VII', 'Preparer'), - b'Zim': ('Irk', 'Invader'), - b'Lrrr': ('Omicron Persei 8', 'Emperor')} + {b'Serak': ('Rigel VII', 'Preparer'), + b'Zim': ('Irk', 'Invader'), + b'Lrrr': ('Omicron Persei 8', 'Emperor')} - Returned keys are always bytes. If a key from the keys argument is - missing from the dictionary, then that row was not found in the - table (and require_all_keys must have been False). + 返回的键值一定是字节串. 如果字典中没有 keys 参数中的某个键值, 说明 + 表格中没有找到这一行 (且 require_all_keys 一定是 false). - Raises: - IOError: An error occurred accessing the smalltable. + 抛出: + IOError: 访问 smalltable 时出现错误. """ -**类** +**类 (class)** - 类应该在其定义下有一个用于描述该类的文档字符串. 如果你的类有公共属性(Attributes), 那么文档中应该有一个属性(Attributes)段. 并且应该遵守和函数参数相同的格式. + 类的定义下方应该有一个描述该类的文档字符串. 如果你的类包含公有属性 (attributes), 应该在 ``Attributes`` (属性) 小节中记录这些属性, 格式与函数的 ``Args`` (参数) 小节类似. .. code-block:: python class SampleClass(object): - """Summary of class here. + """这里是类的概述. - Longer class information.... - Longer class information.... + 这里是更多信息.... + 这里是更多信息.... - Attributes: - likes_spam: A boolean indicating if we like SPAM or not. - eggs: An integer count of the eggs we have laid. + 属性: + likes_spam: 布尔值, 表示我们是否喜欢午餐肉. + eggs: 用整数记录的下蛋的数量. """ - def __init__(self, likes_spam=False): - """Inits SampleClass with blah.""" + def __init__(self, likes_spam = False): + """用某某某初始化 SampleClass.""" self.likes_spam = likes_spam self.eggs = 0 def public_method(self): - """Performs operation blah.""" + """执行某某操作.""" - + 类的文档字符串开头应该是一行概述, 描述类的实例所代表的事物. 这意味着 ``Exception`` 的子类 (subclass) 应该描述这个异常代表什么, 而不是描述抛出异常时的环境. 类的文档字符串不应该有无意义的重复, 例如说这个类是一种类. + + 正确: + + .. code-block:: python + + class CheeseShopAddress: + """奶酪店的地址. + + ... + """ + + class OutOfCheeseError(Exception): + """没有可用的奶酪.""" + + 错误: + + .. code-block:: python + + class CheeseShopAddress: + """一个描述奶酪店地址的类. + + ... + """ + + class OutOfCheeseError(Exception): + """在没有可用的奶酪时抛出.""" **块注释和行注释** - 最需要写注释的是代码中那些技巧性的部分. 如果你在下次 `代码审查 `_ 的时候必须解释一下, 那么你应该现在就给它写注释. 对于复杂的操作, 应该在其操作开始前写上若干行注释. 对于不是一目了然的代码, 应在其行尾添加注释. + 最后一种需要写注释的地方是代码中复杂的部分. 如果你可能在以后 `代码评审 (code review) `_ 时要解释某段代码, 那么现在就应该给这段代码加上注释. 应该在复杂的操作开始前写上若干行注释. 对于不是一目了然的代码, 应该在行尾添加注释. .. code-block:: python - # We use a weighted dictionary search to find out where i is in - # the array. We extrapolate position based on the largest num - # in the array and the array size and then do binary search to - # get the exact number. + # 我们用加权的字典搜索, 寻找 i 在数组中的位置. 我们基于数组中的最大值和数组 + # 长度, 推断一个位置, 然后用二分搜索获得最终准确的结果. - if i & (i-1) == 0: # True if i is 0 or a power of 2. + if i & (i-1) == 0: # 如果 i 是 0 或者 2 的整数次幂, 则为真. - 为了提高可读性, 注释应该至少离开代码2个空格. + 为了提高可读性, 注释的井号和代码之间应有至少2个空格, 井号和注释之间应该至少有一个空格. - 另一方面, 绝不要描述代码. 假设阅读代码的人比你更懂Python, 他只是不知道你的代码要做什么. + 除此之外, 绝不要仅仅描述代码. 应该假设读代码的人比你更懂Python, 只是不知道你的代码要做什么. .. code-block:: python - # BAD COMMENT: Now go through the b array and make sure whenever i occurs - # the next element is i+1 - - -标点符号,拼写和语法 + # 不好的注释: 现在遍历数组 b, 确保每次 i 出现时, 下一个元素是 i+1 + +标点符号、拼写和语法 -------------------- .. tip:: - 注意标点符号,拼写和语法 + 注意标点符号、拼写和语法. 文笔好的注释比差的注释更容易理解. - 注释应有适当的大写和标点,句子应该尽量完整.对于诸如在行尾上的较短注释,可以不那么正式,但是也应该尽量保持风格一致. +注释应该和记叙文一样可读, 使用恰当的大小写和标点. 一般而言, 完整的句子比残缺句更可读. 较短的注释 (比如行尾注释) 可以更随意, 但是你要保持风格一致. - -类 --------------------- - -.. tip:: - 如果一个类不继承自其它类, 就显式的从object继承. 嵌套类也一样.(除非是为了和 python2 兼容) - -.. code-block:: python - - Yes: class SampleClass(object): - pass - - - class OuterClass(object): - - class InnerClass(object): - pass - - - class ChildClass(ParentClass): - """Explicitly inherits from another class already.""" - -.. code-block:: python - - No: class SampleClass: - pass - - - class OuterClass: - - class InnerClass: - pass - -继承自 ``object`` 是为了使属性(properties)正常工作, 并且这样可以保护你的代码, 使其不受 `PEP-3000 `_ 的一个特殊的潜在不兼容性影响. 这样做也定义了一些特殊的方法, 这些方法实现了对象的默认语义, 包括 ``__new__, __init__, __delattr__, __getattribute__, __setattr__, __hash__, __repr__, and __str__`` . +尽管你可能会因为代码审稿人指出你误把冒号写作逗号而灰心, 但是保持源代码清晰可读也是非常重要的. 正确的标点、拼写和语法有助于实现这一目标. 字符串 -------------------- .. tip:: - 即使参数都是字符串, 使用%操作符或者格式化方法格式化字符串. 不过也不能一概而论, 你需要在+和%之间好好判定. + 应该用 `f-string `_、 ``%`` 运算符或 ``format`` 方法来格式化字符串. 即使所有参数都是字符串, 也如此. 你可以自行评判合适的选项. 可以用 ``+`` 实现单次拼接, 但是不要用 ``+`` 实现格式化. -.. code-block:: python - - Yes: x = a + b - x = '%s, %s!' % (imperative, expletive) - x = '{}, {}!'.format(imperative, expletive) - x = 'name: %s; score: %d' % (name, n) - x = 'name: {}; score: {}'.format(name, n) - -.. code-block:: python - - No: x = '%s%s' % (a, b) # use + in this case - x = '{}{}'.format(a, b) # use + in this case - x = imperative + ', ' + expletive + '!' - x = 'name: ' + name + '; score: ' + str(n) - -避免在循环中用+和+=操作符来累加字符串. 由于字符串是不可变的, 这样做会创建不必要的临时对象, 并且导致二次方而不是线性的运行时间. 作为替代方案, 你可以将每个子串加入列表, 然后在循环结束后用 ``.join`` 连接列表. (也可以将每个子串写入一个 ``cStringIO.StringIO`` 缓存中.) +正确: .. code-block:: python - Yes: items = [''] - for last_name, first_name in employee_list: - items.append('' % (last_name, first_name)) - items.append('
%s, %s
') - employee_table = ''.join(items) + 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 + +错误: .. code-block:: python - No: employee_table = '' - for last_name, first_name in employee_list: - employee_table += '' % (last_name, first_name) - employee_table += '
%s, %s
' + x = first + ', ' + second + x = '名称: ' + name + '; 分数: ' + str(n) -在同一个文件中, 保持使用字符串引号的一致性. 使用单引号'或者双引号"之一用以引用字符串, 并在同一文件中沿用. 在字符串内可以使用另外一种引号, 以避免在字符串中使用\. +不要在循环中用 ``+`` 和 ``+=`` 操作符来堆积字符串. 这有时会产生平方而不是线性的时间复杂度. 有时 CPython 会优化这种情况, 但这是一种实现细节. 我们无法轻易预测这种优化是否生效, 而且未来情况可能出现变化. 作为替代方案, 你可以将每个子串加入列表, 然后在循环结束后用 ``''.join`` 拼接列表. 也可以将每个子串写入一个 ``io.StringIO`` 缓冲区中. 这些技巧保证始终有线性的平摊 (amortized) 时间复杂度. + +正确: .. code-block:: python - Yes: - Python('Why are you hiding your eyes?') - Gollum("I'm scared of lint errors.") - Narrator('"Good!" thought a happy Python reviewer.') + items = [''] + for last_name, first_name in employee_list: + items.append('' % (last_name, first_name)) + items.append('
%s, %s
') + employee_table = ''.join(items) + +错误: + +.. code-block:: python + + employee_table = '' + for last_name, first_name in employee_list: + employee_table += '' % (last_name, first_name) + employee_table += '
%s, %s
' + +应该保持同一文件中字符串引号的一致性. 选择 ``'`` 或者 ``"`` 以后不要改变主意. 如果需要避免用反斜杠来转义引号, 则可以使用另一种引号. + +正确: + +.. code-block:: python + + Python('为什么你要捂眼睛?') + Gollum("I'm scared of lint errors. (我害怕格式错误.)") + Narrator('"很好!" 一个开心的 Python 审稿人心想.') + +(译者注: 注意 "I'm" 中间有一个单引号,所以这一行的外层引号可以用不同的引号.) + +错误: .. code-block:: python - No: - Python("Why are you hiding your eyes?") - Gollum('The lint. It burns. It burns us.') - Gollum("Always the great lint. Watching. Watching.") + Python("为什么你要捂眼睛?") + Gollum('格式检查器. 它在闪耀. 它要亮瞎我们.') + Gollum("伟大的格式检查器永在. 它在看. 它在看.") -为多行字符串使用三重双引号"""而非三重单引号'''. 当且仅当项目中使用单引号'来引用字符串时, 才可能会使用三重'''为非文档字符串的多行字符串来标识引用. 文档字符串必须使用三重双引号""". -多行字符串不应随着代码其他部分缩进的调整而发生位置移动. 如果需要避免在字符串中嵌入额外的空间,可以使用串联的单行字符串或者使用 `textwrap.dedent() `_ 来删除每行多余的空间. +多行字符串推荐使用 ``"""`` 而非 ``'''``. 当且仅当项目中用 ``'`` 给常规字符串打引号时, 才能在文档字符串以外的多行字符串上使用 ``'''``. 无论如何, 文档字符串必须使用 ``"""``. + +多行字符串不会跟进代码其他部分的缩进. 如果需要避免字符串中的额外空格, 可以用多个单行字符串拼接, 或者用 `textwrap.dedent() `_ 删除每行开头的空格. + +错误: .. code-block:: python - No: - long_string = """This is pretty ugly. - Don't do this. + long_string = """这样很难看. + 不要这样做. """ - -.. code-block:: python - Yes: - long_string = """This is fine if your use case can accept - extraneous leading spaces.""" +正确: .. code-block:: python - Yes: - long_string = ("And this is fine if you cannot accept\n" + - "extraneous leading spaces.") + long_string = """如果你可以接受多余的空格, + 就可以这样.""" + + long_string = ("如果你不能接受多余的空格,\n" + + "可以这样.") + + long_string = ("如果你不能接受多余的空格,\n" + "也可以这样.") .. code-block:: python - Yes: - long_string = ("And this too is fine if you cannot accept\n" - "extraneous leading spaces.") -.. code-block:: python - - Yes: import textwrap long_string = textwrap.dedent("""\ - This is also fine, because textwrap.dedent() - will collapse common leading spaces in each line.""") + 这样也行, 因为 textwrap.dedent() + 会删除每一行开头共有的空格.""") -文件和sockets --------------------- +注意, 这里的反斜杠没有违反 :ref:`显式续行的禁令 `. 此时, 反斜杠用于在字符串字面量 (literal) 中 `对换行符转义 `_. + +**日志** + + 对于那些第一个参数是格式字符串 (包含 ``%`` 占位符) 的日志函数: 一定要用字符串字面量 (而非 f-string!) 作为第一个参数, 并用占位符的参数作为其他参数. 有些日志的实现会收集未展开的格式字符串, 作为可搜索的项目. 这样也可以免于渲染那些被设置为不用输出的消息. + + 正确; + + .. code-block:: python + + import tensorflow as tf + logger = tf.get_logger() + logger.info('TensorFlow 的版本是: %s', tf.__version__) + + .. code-block:: python + + 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) + + 错误: + + .. code-block:: python + + 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). + + 正确: + + .. code-block:: python + + if not 0 <= p <= 1: + raise ValueError(f'这不是概率值: {p!r}') + + try: + os.rmdir(workdir) + except OSError as error: + logging.warning('无法删除这个文件夹 (原因: %r): %r', + error, workdir) + + 错误: + + .. code-block:: python + + 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) + +文件、套接字 (socket) 和类似的有状态资源 +-------------------------------------------- .. tip:: - 在文件和sockets结束时, 显式的关闭它. + 使用完文件和套接字以后, 显式地关闭它们. 自然地, 这条规则也应该扩展到其他在内部使用套接字的可关闭资源 (比如数据库连接) 和其他需要用类似方法关停的资源. 其他例子还有 `mmap `_ 映射、 `h5py 的文件对象 `_ 和 `matplotlib.pyplot 的图像窗口 `_ . -除文件外, sockets或其他类似文件的对象在没有必要的情况下打开, 会有许多副作用, 例如: +如果保持不必要的文件、套接字或其他有状态对象开启, 会产生很多缺点: -#. 它们可能会消耗有限的系统资源, 如文件描述符. 如果这些资源在使用后没有及时归还系统, 那么用于处理这些对象的代码会将资源消耗殆尽. -#. 持有文件将会阻止对于文件的其他诸如移动、删除之类的操作. -#. 仅仅是从逻辑上关闭文件和sockets, 那么它们仍然可能会被其共享的程序在无意中进行读或者写操作. 只有当它们真正被关闭后, 对于它们尝试进行读或者写操作将会抛出异常, 并使得问题快速显现出来. +#. 它们可能消耗有限的系统资源, 例如文件描述符. 如果代码需要使用大量类似的资源而没有及时返还给系统, 就有可能出现原本可以避免的资源枯竭情况. +#. 保持文件的开启状态会阻碍其他操作, 例如移动、删除文件, 卸载 (unmont) 文件系统等等. +#. 如果程序的多个部分共享文件和套接字, 即使逻辑上文件已经关闭了, 仍然有可能出现意外的读写操作. 如果这些资源真正关闭了, 读写操作会抛出异常, 让问题早日浮出水面. -而且, 幻想当文件对象析构时, 文件和sockets会自动关闭, 试图将文件对象的生命周期和文件的状态绑定在一起的想法, 都是不现实的. 因为有如下原因: +此外, 即使文件和套接字 (以及其他行为类似的资源) 会在析构 (destruct) 时自动关闭, 把对象的生命周期和资源状态绑定的行为依然不妥: -#. 没有任何方法可以确保运行环境会真正的执行文件的析构. 不同的Python实现采用不同的内存管理技术, 比如延时垃圾处理机制. 延时垃圾处理机制可能会导致对象生命周期被任意无限制的延长. +#. 无法保证运行时 (runtime) 调用 ``__del__`` 方法的真正时机. 不同的 Python 实现采用了不同的内存管理技巧 (比如延迟垃圾处理机制, delayed garbage collection), 可能会随意、无限期地延长对象的生命周期. +#. 意想不到的文件引用 (例如全局对象和异常的堆栈跟踪, exception tracebacks) 可能让文件的存续时间比想象的更长. -#. 对于文件意外的引用,会导致对于文件的持有时间超出预期(比如对于异常的跟踪, 包含有全局变量等). +依赖于终结器 (finalizer) 实现自动清理的方法有显著的副作用. 这在几十年的时间里、在多种语言中 (参见 `这篇 `_ Java 的文章) 多次引发严重问题. -推荐使用 `"with"语句 `_ 以管理文件: +推荐使用 `"with"语句 `_ 管理文件和类似的资源: .. code-block:: python @@ -649,7 +688,7 @@ Shebang for line in hello_file: print line -对于不支持使用"with"语句的类似文件的对象,使用 contextlib.closing(): +对于不支持 ``with`` 语句且类似文件的对象, 应该使用 ``contextlib.closing()``: .. code-block:: python @@ -659,69 +698,83 @@ Shebang for line in front_page: print line -Legacy AppEngine 中Python 2.5的代码如使用"with"语句, 需要添加 ``from __future__ import with_statement`` . +少数情况下无法使用基于上下文 (context) 的资源管理, 此时文档应该清楚地解释代码会如何管理资源的生命周期. - -TODO注释 +TODO (待办) 注释 -------------------- .. tip:: - 为临时代码使用TODO注释, 它是一种短期解决方案. 不算完美, 但够好了. + 在临时、短期和不够完美的代码上添加 TODO (待办) 注释. -TODO注释应该在所有开头处包含"TODO"字符串, 紧跟着是用括号括起来的你的名字, email地址或其它标识符. 然后是一个可选的冒号. 接着必须有一行注释, 解释要做什么. 主要目的是为了有一个统一的TODO格式, 这样添加注释的人就可以搜索到(并可以按需提供更多细节). 写了TODO注释并不保证写的人会亲自解决问题. 当你写了一个TODO, 请注上你的名字. +待办注释以 ``TODO`` (待办) 这个全部大写的词开头, 紧跟着是用括号括起来的上下文标识符 (最好是 bug 链接, 有时是你的用户名). 最好是诸如 ``TODO(https://crbug.com/):`` 这样的 bug 链接, 因为 bug 有历史追踪和评论, 而程序员可能发生变动并忘记上下文. TODO 后面应该解释待办的事情. -.. code-block:: python +统一 TODO 的格式是为了方便搜索并查看详情. TODO 不代表注释中提到的人要做出修复问题的保证. 所以, 当你创建带有用户名的 TODO 时, 大部分情况下应该用你自己的用户名. - # TODO(kl@gmail.com): Use a "*" here for string repetition. - # TODO(Zeke) Change this to use relations. +.. code-block:: python + + # TODO(crbug.com/192795): 研究 cpufreq 的优化. + # TODO(你的用户名): 提交一个议题 (issue), 用 '*' 代表重复. -如果你的TODO是"将来做某事"的形式, 那么请确保你包含了一个指定的日期("2009年11月解决")或者一个特定的事件("等到所有的客户都可以处理XML请求就移除这些代码"). +如果你的 TODO 形式类似于"将来做某事", 请确保其中包含特别具体的日期 ("2009年11月前解决") 或者特别具体的事件 ("当所有客户端都能处理 XML 响应时, 删除这些代码"), 以便于未来的代码维护者理解. -导入格式 --------------------- +导入 (import) 语句的格式 +------------------------- .. tip:: - 每个导入应该独占一行, ``typing`` 的导入除外 + 导入语句应该各自独占一行. :ref:`typing 和 collections.abc 的导入除外 `. 例如: + +正确: .. code-block:: python - Yes: import os - import sys - from typing import Mapping, Sequence + from collections.abc import Mapping, Sequence + import os + import sys + from typing import Any, NewType + +错误: .. code-block:: python - No: import os, sys + import os, sys -导入总应该放在文件顶部, 位于模块注释和文档字符串之后, 模块全局变量和常量之前. 导入应该按照从最通用到最不通用的顺序分组: +导入语句必须在文件顶部, 位于模块的注释和文档字符串之后、全局变量和全局常量之前. 导入语句应该按照如下顺序分组, 从通用到特殊: -#. ``__future__`` 导入 +#. 导入 Python 的 ``__future__``. 例如: -.. code-block:: python + .. code-block:: python - from __future__ import absolute_import - from __future__ import division - from __future__ import print_function + from __future__ import annotations -#. 标准库导入 + 参见前文有关 ``__future__`` 语句的描述. -.. code-block:: python +#. 导入 Python 的标准库. 例如: - import sys + .. code-block:: python -#. 第三方库导入 + import sys -.. code-block:: python - - import tensorflow as tf +#. 导入 `第三方 `_ 模块和包. 例如: -#. 本地代码子包导入 + .. code-block:: python -.. code-block:: python + import tensorflow as tf - from otherproject.ai import mind +#. 导入代码仓库中的子包. 例如: -每种分组中, 应该根据每个模块的完整包路径按字典序排序, 忽略大小写. + .. code-block:: python + + from otherproject.ai import mind + +#. **已废弃的规则**: 导入应用专属的、与该文件属于同一个子包的模块. 例如: + + .. code-block:: python + + from myproject.backend.hgwells import time_machine + + 你可能会在较老的谷歌风格 Python 代码中遇到这样的模式, 但现在不再执行这条规则. **我们建议新代码忽略这条规则.** 同等对待应用专属的子包和其他子包即可. + +在每个分组内部, 应该按照模块完整包路径 (例如 ``from path import ...`` 中的 ``path``) 的字典序排序, 忽略大小写. 可以选择在分组之间插入空行. .. code-block:: python @@ -743,7 +796,7 @@ TODO注释应该在所有开头处包含"TODO"字符串, 紧跟着是用括号 from otherproject.ai import mind from otherproject.ai import soul - # Older style code may have these imports down here instead: + # 旧的代码可能会把这些导入语句放在下面这里: #from myproject.backend.hgwells import time_machine #from myproject.backend.state_machine import main_loop @@ -751,19 +804,19 @@ TODO注释应该在所有开头处包含"TODO"字符串, 紧跟着是用括号 -------------------- .. tip:: - 通常每个语句应该独占一行 + 通常每个语句应该独占一行. -不过, 如果测试结果与测试语句在一行放得下, 你也可以将它们放在同一行. 如果是if语句, 只有在没有else时才能这样做. 特别地, 绝不要对 ``try/except`` 这样做, 因为try和except不能放在同一行. +不过, 如果判断语句的主体与判断条件可以挤进一行, 你可以将它们放在同一行. 特别注意这不适用于 ``try`` / ``except``, 因为 ``try`` 和 ``except`` 不能放在同一行. 只有在 ``if`` 语句没有对应的 ``else`` 时才适用. + +正确: .. code-block:: python - - Yes: - if foo: bar(foo) + if foo: bar(foo) + +错误: .. code-block:: python - - No: if foo: bar(foo) else: baz(foo) @@ -774,72 +827,157 @@ TODO注释应该在所有开头处包含"TODO"字符串, 紧跟着是用括号 try: bar(foo) except ValueError: baz(foo) - - -访问控制 --------------------- + +.. _getter_setter: + +访问器 (getter) 和设置器 (setter) +-------------------------------------- .. tip:: - 在Python中, 对于琐碎又不太重要的访问函数, 你应该直接使用公有变量来取代它们, 这样可以避免额外的函数调用开销. 当添加更多功能时, 你可以用属性(property)来保持语法的一致性. - - (译者注: 重视封装的面向对象程序员看到这个可能会很反感, 因为他们一直被教育: 所有成员变量都必须是私有的! 其实, 那真的是有点麻烦啊. 试着去接受Pythonic哲学吧) - -另一方面, 如果访问更复杂, 或者变量的访问开销很显著, 那么你应该使用像 ``get_foo()`` 和 ``set_foo()`` 这样的函数调用. 如果之前的代码行为允许通过属性(property)访问 , 那么就不要将新的访问函数与属性绑定. 这样, 任何试图通过老方法访问变量的代码就没法运行, 使用者也就会意识到复杂性发生了变化. + 在访问和设置变量值时, 如果访问器和设置器 (又名为访问子 accessor 和变异子 mutator) 可以产生有意义的作用或效果, 则可以使用. + +特别来说, 如果在当下或者可以预见的未来, 读写某个变量的过程很复杂或者成本高昂, 则应该使用这种函数. + +如果一对访问器和设置器仅仅用于读写一个内部属性 (attribute), 你应该直接用公有属性取代它们. 相较而言, 如果设置操作会让部分状态无效化或引发重建, 则需要使用设置器. 显式的函数调用表示可能出现特殊的操作. 如果只有简单的逻辑, 或者在重构代码后不再需要访问器和设置器, 你可以用属性 (property) 替代. + +(译者注: 重视封装的面向对象程序员看到这个可能会很反感, 因为他们一直被教育: 所有成员变量都必须是私有的! 其实, 那真的是有点麻烦啊. 试着去接受Pythonic哲学吧) + +访问器和设置器应该遵守命名规范, 例如 ``get_foo()`` 和 ``set_foo()``. + +如果之前的代码通过属性获取数据, 则不能把重新编写的访问器/设置器与这一属性绑定. 应该让任何用老办法访问变量的代码出现显眼的错误, 让使用者意识到代码复杂度有变化. 命名 -------------------- .. tip:: - 模块名写法: ``module_name`` ;包名写法: ``package_name`` ;类名: ``ClassName`` ;方法名: ``method_name`` ;异常名: ``ExceptionName`` ;函数名: ``function_name`` ;全局常量名: ``GLOBAL_CONSTANT_NAME`` ;全局变量名: ``global_var_name`` ;实例名: ``instance_var_name`` ;函数参数名: ``function_parameter_name`` ;局部变量名: ``local_var_name`` . - 函数名,变量名和文件名应该是描述性的,尽量避免缩写,特别要避免使用非项目人员不清楚难以理解的缩写,不要通过删除单词中的字母来进行缩写. - 始终使用 ``.py`` 作为文件后缀名,不要用破折号. + 模块名: ``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`` 作为文件后缀名. 不要用连字符. + +**需要避免的名称** - #. 单字符名称, 除了计数器和迭代器,作为 ``try/except`` 中异常声明的 ``e``,作为 ``with`` 语句中文件句柄的 ``f``. - #. 包/模块名中的连字符(-) - #. 双下划线开头并结尾的名称(Python保留, 例如__init__) + #. 只有单个字符的名称, 除了以下特别批准的情况: + + #. 计数器和迭代器 (例如, ``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)"表示仅模块内可用, 或者, 在类内是保护或私有的. - #. 用单下划线(_)开头表示模块变量或函数是protected的(使用from module import \*时不会包含). - #. 用双下划线(__)开头的实例变量或方法表示类内私有. - #. 将相关的类和顶级函数放在同一个模块里. 不像Java, 没必要限制一个类一个模块. - #. 对类名使用大写字母开头的单词(如CapWords, 即Pascal风格), 但是模块名应该用小写加下划线的方式(如lower_with_under.py). 尽管已经有很多现存的模块使用类似于CapWords.py这样的命名, 但现在已经不鼓励这样做, 因为如果模块名碰巧和类名一致, 这会让人困扰. + #. "内部(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`` 为后缀名且不包含 ``-``.若是需要一个无后缀名的可执行文件,可以使用软联接或者包含 ``exec "$0.py" "$@"`` 的bash脚本. + 所有 Python 文件名都应该以 ``.py`` 为文件后缀且不能包含连字符 (``-``). 这样便于导入这些文件并编写单元测试. 如果想通过不含后缀的命令运行程序, 可以使用软链接文件 (symbolic link) 或者 ``exec "$0.py" "$@"`` 这样简单的 bash 脚本. -**Python之父Guido推荐的规范** +**根据Python之父Guido的建议所制定的规范** -=========================== ==================== ====================================================================== -Type Public Internal -=========================== ==================== ====================================================================== -Modules lower_with_under _lower_with_under -Packages lower_with_under -Classes CapWords _CapWords -Exceptions CapWords -Functions lower_with_under() _lower_with_under() -Global/Class Constants CAPS_WITH_UNDER _CAPS_WITH_UNDER -Global/Class Variables lower_with_under _lower_with_under -Instance Variables lower_with_under _lower_with_under (protected) or __lower_with_under (private) -Method Names lower_with_under() _lower_with_under() (protected) or __lower_with_under() (private) -Function/Method Parameters lower_with_under -Local Variables lower_with_under -=========================== ==================== ====================================================================== +.. list-table:: 描述 + :widths: 30 30 40 + :header-rows: 1 + * - 类型 + - 公有 + - 内部 + * - 包 + - 小写下划线 + - + * - 模块 + - 小写下划线 + - 下划线+小写下划线 + * - 类 + - 大驼峰 + - 下划线+大驼峰 + * - 异常 + - 大驼峰 + - + * - 函数 + - 小写下划线 + - 下划线+小写下划线 + * - 全局常量/类常量 + - 大写下划线 + - 下划线+大写下划线 + * - 全局变量/类变量 + - 小写下划线 + - 下划线+小写下划线 + * - 实例变量 + - 小写下划线 + - 下划线+小写下划线 (受保护) + * - 方法名 + - 小写下划线 + - 下划线+小写下划线 (受保护) + * - 函数参数/方法参数 + - 小写下划线 + - + * - 局部变量 + - 小写下划线 + - -Main +.. list-table:: 例子 + :widths: 30 35 35 + :header-rows: 1 + + * - 类型 + - 公有 + - 内部 + * - 包 + - ``lower_with_under`` + - + * - 模块 + - ``lower_with_under`` + - ``_lower_with_under`` + * - 类 + - ``CapWords`` + - ``_CapWords`` + * - 异常 + - ``CapWords`` + - + * - 函数 + - ``lower_with_under()`` + - ``_lower_with_under()`` + * - 全局常量/类常量 + - ``CAPS_WITH_UNDER`` + - ``_CAPS_WITH_UNDER`` + * - 全局变量/类变量 + - ``lower_with_under`` + - ``_lower_with_under`` + * - 实例变量 + - ``lower_with_under`` + - ``_lower_with_under`` + * - 方法名 + - ``lower_with_under()`` + - ``_lower_with_under()`` + * - 函数参数/方法参数 + - ``lower_with_under`` + - + * - 局部变量 + - ``lower_with_under`` + - + +**数学符号** + +对于涉及大量数学内容的代码, 如果相关论文或算法中有对应的符号, 则可以忽略以上命名规范并使用较短的变量名. 若要采用这种方法, 应该在注释或者文档字符串中注明你所使用的命名规范的来源. 如果原文无法访问, 则应该在文档中清楚地记录命名规范. 建议公开的 API 使用符合 PEP8 的、描述性的名称, 因为使用 API 的代码很可能缺少相关的上下文信息. + +主程序 -------------------- .. tip:: - 即使是一个打算被用作脚本的文件, 也应该是可导入的. 并且简单的导入不应该导致这个脚本的主功能(main functionality)被执行, 这是一种副作用. 主功能应该放在一个main()函数中. + 使用 Python 时, 提供给 ``pydoc`` 和单元测试的模块必须是可导入的. 如果一个文件是可执行文件, 该文件的主要功能应该位于 ``main()`` 函数中. 你的代码必须在执行主程序前检查 ``if __name__ == '__main__'`` , 这样导入模块时不会执行主程序. -在Python中, pydoc以及单元测试要求模块必须是可导入的. 你的代码应该在执行主程序前总是检查 ``if __name__ == '__main__'`` , 这样当模块被导入时主程序就不会被执行. - -若使用 `absl `_, 请使用 ``app.run`` : +使用 `absl `_ 时, 请调用 ``app.run`` : .. code-block:: python @@ -847,13 +985,13 @@ Main ... def main(argv): - # process non-flag arguments + # 处理非标志 (non-flag) 参数 ... if __name__ == '__main__': app.run(main) -否则,使用: +否则, 使用: .. code-block:: python @@ -863,232 +1001,293 @@ Main if __name__ == '__main__': main() -所有的顶级代码在模块导入时都会被执行. 要小心不要去调用函数, 创建对象, 或者执行那些不应该在使用pydoc时执行的操作. +导入模块时会执行该模块的所有顶级代码. 注意顶级代码中不能有 ``pydoc`` 不该执行的操作, 比如调用函数, 创建对象等. 函数长度 -------------------- .. tip:: - 推荐函数功能尽量集中,简单,小巧 + 函数应该小巧且专一. -不对函数长度做硬性限制.但是若一个函数超过来40行,推荐考虑一下是否可以在不损害程序结构的情况下对其进行分解. -因为即使现在长函数运行良好,但几个月后可能会有人修改它并添加一些新的行为,这容易产生难以发现的bug.保持函数的简练,使其更加容易阅读和修改. -当遇到一些很长的函数时,若发现调试比较困难或是想在其他地方使用函数的一部分功能,不妨考虑将这个场函数进行拆分. +我们承认有时长函数也是合理的, 所以不硬性限制函数长度. 若一个函数超过 40 行, 应该考虑在不破坏程序结构的前提下拆分这个函数. +即使一个长函数现在没有问题, 几个月后可能会有别人添加新的效果. 此时容易出现隐蔽的错误. 保持函数简练, 这样便于别人阅读并修改你的代码. -类型注释 --------------------- +当你使用某些代码时, 可能发现一些冗长且复杂的函数. 要勇于修改现有的代码: 如果该函数难以使用或者存在难以调试的错误, 亦或是你想在不同场景下使用该函数的片段, 不妨考虑把函数拆分成更小、更容易管理的片段. + +类型注解 (type annotation) +------------------------------- **通用规则** - #. 请先熟悉下 'PEP-484 '_ - #. 对于方法,仅在必要时才对 ``self`` 或 ``cls`` 注释 - #. 若对类型没有任何显示,请使用 ``Any`` - #. 无需注释模块中的所有函数 - #. 公共的API需要注释 - #. 在代码的安全性,清晰性和灵活性上进行权衡是否注释 - #. 对于容易出现类型相关的错误的代码进行注释 - #. 难以理解的代码请进行注释 - #. 若代码中的类型已经稳定,可以进行注释. 对于一份成熟的代码,多数情况下,即使注释了所有的函数,也不会丧失太多的灵活性. + #. 熟读 `PEP-484 `_ . + #. 仅在有额外类型信息时才需要注解方法中 ``self`` 或 ``cls`` 的类型. 例如: + + .. code-block:: python + + @classmethod + def create(cls: Type[_T]) -> _T: + return cls() + + #. 类似地, 不需要注解 ``__init__`` 的返回值 (只能返回 ``None``). + #. 对于其他不需要限制变量类型或返回类型的情况, 应该使用 ``Any``. + #. 无需注解模块中的所有函数. + + #. 至少需要注解你的公开 API. + #. 你可以自行权衡, 一方面要保证代码的安全性和清晰性, 另一方面要兼顾灵活性. + #. 应该注解那些容易出现类型错误的代码 (比如曾经出现过错误或疑难杂症). + #. 应该注解晦涩难懂的代码. + #. 应该注解那些类型已经确定的代码. 多数情况下,即使注解了成熟的代码中所有的函数,也不会丧失太多灵活性. **换行** + + 尽量遵守前文所述的缩进规则. - 尽量遵守既定的缩进规则.注释后,很多函数签名将会变成每行一个参数. - - .. code-block:: python - - def my_method(self, - first_var: int, - second_var: Foo, - third_var: Optional[Bar]) -> int: - ... - - - 尽量在变量之间换行而不是在变量和类型注释之间.当然,若所有东西都在一行上,也可以接受. - - .. code-block:: python - - def my_method(self, first_var: int) -> int: - ... - - 若是函数名,末位形参和返回值的类型注释太长,也可以进行换行,并在新行进行4格缩进. - - .. code-block:: python - - def my_method( - self, first_var: int) -> Tuple[MyLongType1, MyLongType1]: - ... - - 若是末位形参和返回值类型注释不适合在同一行上,可以换行,缩进为4空格,并保持闭合的括号 ``)`` 和 ``def`` 对齐 - - .. code-block:: python - - Yes: - def my_method( - self, other_arg: Optional[MyLongType] - ) -> Dict[OtherLongType, MyLongType]: - ... - - ``pylint`` 允许闭合括号 ``)`` 换至新行并与 开启括号 ``(`` 对齐,但这样的可读性不好. - - .. code-block:: python - - No: - def my_method(self, - other_arg: Optional[MyLongType] - ) -> Dict[OtherLongType, MyLongType]: - ... - - 如上所示,尽量不要在一个类型注释中进行换行.但是有时类型注释过长需要换行时,请尽量保持子类型中不被换行. + 添加类型注解后, 很多函数签名 (signature) 会变成每行一个参数的形式. 若要让返回值单独成行, 可以在最后一个参数尾部添加逗号. .. code-block:: python def my_method( self, - first_var: Tuple[List[MyLongType1], - List[MyLongType2]], - second_var: List[Dict[ - MyLongType3, MyLongType4]]) -> None: - ... - - 若一个类型注释确实太长,则应优先考虑对过长的类型使用别名 `alias `_. 其次是考虑在冒号后 ``:``进行换行并添加4格空格缩进. + first_var: int, + second_var: Foo, + third_var: Bar | None, + ) -> int: + ... + 尽量在变量之间换行, 避免在变量和类型注解之间换行. 当然, 若所有东西可以挤进一行, 也可以接受. + + .. code-block:: python + + def my_method(self, first_var: int) -> int: + ... + + 若最后一个参数加上返回值的类型注解太长, 也可以换行并添加4格缩进. 添加换行符时, 建议每个参数和返回值都在单独的一行里, 并且右括号和 ``def`` 对齐. + + 正确: + + .. code-block:: python + + def my_method( + self, + other_arg: MyLongType | None, + ) -> tuple[MyLongType1, MyLongType1]: + ... + + 返回值类型和最后一个参数也可以放在同一行. + + 可以接受: + + .. code-block:: python + + def my_method( + self, + first_var: int, + second_var: int) -> dict[OtherLongType, MyLongType]: + ... + + ``pylint`` 也允许你把右括号放在新行上, 与左括号对齐, 但相较而言可读性更差. + + 错误: + + .. code-block:: python + + def my_method(self, + other_arg: MyLongType | None, + ) -> dict[OtherLongType, MyLongType]: + ... + + 正如上面所有的例子, 尽量不要在类型注解中间换行. 但是有时注解过长以至于一行放不下. 此时尽量保持子类型中间不换行. + + .. code-block:: python + + def my_method( + self, + first_var: tuple[list[MyLongType1], + list[MyLongType2]], + second_var: list[dict[ + MyLongType3, MyLongType4]], + ) -> None: + ... + + 若某个名称和对应的类型注解过长, 可以考虑用 :ref:`别名 (alias) ` 代表类型. 下策是在冒号后换行并添加4格缩进. + + 正确: + .. code-block:: python - Yes: def my_function( long_variable_name: long_module_name.LongTypeName, ) -> None: - ... + ... + + 错误: .. code-block:: python - No: def my_function( long_variable_name: long_module_name. LongTypeName, ) -> None: - ... + ... -**预先声明** +**前向声明 (foward declaration)** - 若需要使用一个当前模块尚未定义的类名,比如想在类声明中使用类名,请使用类名的字符串 + 若需要使用一个尚未定义的类名 (比如想在声明一个类时使用自身的类名), 可以使用 ``from __future__ import annotations`` 或者字符串来代表类名. + + 正确: .. code-block:: python + from __future__ import annotations + class MyClass: + def __init__(self, stack: Sequence[MyClass], item: OtherClass) -> None: - def __init__(self, - stack: List["MyClass"]) -> None: - -**参数默认值** - - 依据 `PEP-008 `_ ,仅对同时具有类型注释和默认值的参数的 ``=`` 周围加空格. + class OtherClass: + ... + + .. code-block:: python + + class MyClass: + def __init__(self, stack: Sequence['MyClass'], item: 'OtherClass') -> None: + + class OtherClass: + ... + +**默认值** + + 根据 `PEP-008 `_ , **只有** 对于同时拥有类型注解和默认值的参数, ``=`` 的周围应该加空格. + + 正确: .. code-block:: python - Yes: def func(a: int = 0) -> int: - ... + ... + + 错误: .. code-block:: python - No: def func(a:int=0) -> int: - ... + ... **NoneType** - 在python的类型系统中, ``NoneType`` 是 "一等对象",为了输入方便, ``None`` 是 ``NoneType`` 的别名.一个变量若是 ``None``,则该变量必须被声明.我们可以使用 ``Union``, 但若类型仅仅只是对应另一个其他类型,建议使用 ``Optional``. - 尽量显式而非隐式的使用 ``Optional``.在PEP-484的早期版本中允许使用 ``a: Text = None`` 来替代 ``a: Optional[Text] = None``,当然,现在不推荐这么做了. + 在 Python 的类型系统中, ``NoneType`` 是 "一等" 类型. 在类型注解中, ``None`` 是 ``NoneType`` 的别名. 如果一个变量可能为 ``None``, 则必须声明这种情况! 你可以使用 ``|`` 这样的联合 (union) 类型表达式 (推荐在新的 Python 3.10+ 代码中使用) 或者老的 ``Optional`` 和 ``Union`` 语法. + + 应该用显式的 ``X | None`` 替代隐式声明. 早期的 PEP 484 允许将 ``a: str = None`` 解释为 ``a: str | None = None``, 但这不再是推荐的行为. + + 正确: .. code-block:: python - Yes: - def func(a: Optional[Text], b: Optional[Text] = None) -> Text: + # 现代的联合写法. + def modern_or_union(a: str | int | None, b: str | None = None) -> str: ... - def multiple_nullable_union(a: Union[None, Text, int]) -> Text + # 采用 Union / Optional. + def union_optional(a: Union[str, int, None], b: Optional[str] = None) -> str: ... + 错误: + .. code-block:: python - No: - def nullable_union(a: Union[None, Text]) -> Text: + # 用 Union 代替 Optional. + def nullable_union(a: Union[None, str]) -> str: ... - def implicit_optional(a: Text = None) -> Text: + # 隐式 Optional. + def implicit_optional(a: str = None) -> str: ... -**类型别名** +.. _type_alias: - 复杂类型应使用别名,别名的命名可参照帕斯卡命名.若别名仅在当前模块使用,应在名称前加``_``变为私有的. - 如下例子中,模块名和类型名连一起过长: +**类型别名 (alias)** + + 你可以为复杂的类型声明一个别名. 别名的命名应该采用大驼峰 (例如 ``CapWorded``). 若别名仅在当前模块使用, 应在名称前加 ``_`` 代表私有 (例如 ``_Private``). + + 注意下面的 ``: TypeAlias`` 类型注解只能在 3.10 以后的版本使用. .. code-block:: python - _ShortName = module_with_long_name.TypeWithLongName - ComplexMap = Mapping[Text, List[Tuple[int, int]]] + from typing import TypeAlias -**忽略类型注释** + _LossAndGradient: TypeAlias = tuple[tf.Tensor, tf.Tensor] + ComplexTFMap: TypeAlias = Mapping[str, _LossAndGradient] + +**忽略类型** - 可以使用特殊的行尾注释 ``# type: ignore`` 来禁用该行的类型检查. - ``pytype`` 针对特定错误有一个禁用选项(类似lint): + 你可以使用特殊的注释 ``# type: ignore`` 禁用某一行的类型检查. + + ``pytype`` 有针对特定错误的禁用选项 (类似格式检查器): .. code-block:: python # pytype: disable=attribute-error -**变量类型注解** +**标注变量的类型** - 当一个内部变量难以推断其类型时,可以有以下方法来指示其类型: - - **类型注释** + **带类型注解的赋值** - 使用行尾注释 ``# type:``: + 如果难以自动推理某个内部变量的类型, 可以用带类型注解的赋值操作来指定类型: 在变量名和值的中间添加冒号和类型, 类似于有默认值的函数参数. .. code-block:: python - a = SomeUndecoratedFunction() # type: Foo - - **带类型注解的复制** - 如函数形参一样,在变量名和等号间加入冒号和类型: - - .. code-block:: python - a: Foo = SomeUndecoratedFunction() -**Tuples vs Lists** + **类型注释** + + 你可能在代码仓库中看到这种残留的注释 (在 Python 3.6 之前必须这样写注释), 但是不要再添加 ``# type: <类型>`` 这样的行尾注释了: + + .. code-block:: python + + a = SomeUndecoratedFunction() # type: Foo + +**元组还是列表** + + 有类型的列表中只能有一种类型的元素. 有类型的元组可以有相同类型的元素或者若干个不同类型的元素. 后面这种情况多用于注解返回值的类型. - 类型化的Lists只能包含单一类型的元素.但类型化的Tuples可以包含单一类型的元素或者若干个不同类型的元素,通常被用来注解返回值的类型. (译者注: 注意这里是指的类型注解中的写法,实际python中,list和tuple都是可以在一个序列中包含不同类型元素的,当然,本质其实list和tuple中放的是元素的引用) .. code-block:: python - a = [1, 2, 3] # type: List[int] - b = (1, 2, 3) # type: Tuple[int, ...] - c = (1, "2", 3.5) # type: Tuple[int, Text, float] + a: list[int] = [1, 2, 3] + b: tuple[int, ...] = (1, 2, 3) + c: tuple[int, str, float] = (1, "2", 3.5) -**TypeVars** +**类型变量 (type variable)** - python的类型系统是支持泛型的.一种常见的方式就是使用工厂函数 ``TypeVars``. + Python 的类型系统支持 `泛型 (generics) `_ . 使用泛型的常见方式是利用类型变量, 例如 ``TypeVar`` 和 ``ParamSpec``. + + 例如: .. code-block:: python - from typing import List, TypeVar - T = TypeVar("T") + from collections.abc import Callable + from typing import ParamSpec, TypeVar + _P = ParamSpec("_P") + _T = TypeVar("_T") ... - def next(l: List[T]) -> T: + def next(l: list[_T]) -> _T: return l.pop() - TypeVar也可以被限定成若干种类型 + def print_when_called(f: Callable[_P, _T]) -> Callable[_P, _T]: + def inner(*args: P.args, **kwargs: P.kwargs) -> R: + print('函数被调用') + return f(*args, **kwargs) + return inner + + ``TypeVar`` 可以有约束条件. .. code-block:: python - AddableType = TypeVar("AddableType", int, float, Text) + AddableType = TypeVar("AddableType", int, float, str) def add(a: AddableType, b: AddableType) -> AddableType: return a + b - ``typing`` 模块中一个常见的预定义类型变量是 ``AnyStr``.它可以用来注解类似 ``bytes``, ``unicode`` 以及一些相似类型. + ``AnyStr`` 是 ``typing`` 模块中常用的预定义类型变量. 可以用它注解那些接受 ``bytes`` 或 ``str`` 但是必须保持一致的类型. .. code-block:: python @@ -1097,76 +1296,95 @@ Main if len(x) <= 42: return x raise ValueError() + + (译者注: 这个例子中, x 和返回值必须同时是 ``bytes`` 或者同时是 ``str``.) + + 类型变量必须有描述性的名称, 除非满足以下所有标准: + + #. 外部不可见 + #. 没有约束条件 + + 正确: + + .. code-block:: python + + _T = TypeVar("_T") + _P = ParamSpec("_P") + AddableType = TypeVar("AddableType", int, float, str) + AnyFunction = TypeVar("AnyFunction", bound=Callable) + + 错误: + + .. code-block:: python + + T = TypeVar("T") + P = ParamSpec("P") + _T = TypeVar("_T", int, float, str) + _F = TypeVar("_F", bound=Callable) **字符串类型** - 如何正确的注释字符串的相关类型和要使用的python版本有关. - 对于仅在 python3 下运行的代码,首选使用 ``str``. 使用 ``Text`` 也可以.但是两个不要混用,保持风格一致. - 对于需要兼容 python2 的代码,使用 ``Text``.在少数情况下,使用 ``str`` 也许更加清晰.不要使用 ``unicode``,因为 python3 里没有这个类型. - 造成这种差异的原因是因为,在不同的python版本中,``str`` 意义不同. - - .. code-block:: python - - No: - def py2_code(x: str) -> unicode: - ... - - 对于需要处理二进制数据的代码,使用 ``bytes``. + 不要在新代码中使用 ``typing.Text``. 这种写法只能用于处理 Python 2/3 的兼容问题. + + 用 ``str`` 表示字符串/文本数据. 用 ``bytes`` 处理二进制数据. .. code-block:: python + # 处理文本数据 + def deals_with_text_data(x: str) -> str: + ... + # 处理二进制数据 def deals_with_binary_data(x: bytes) -> bytes: - ... + ... - python2 中的文本类数据类型包括``str``和``unicode``,而python3 中仅有 ``str``. + 若一个函数中的字串类型始终一致, 比如上述代码中返回值类型和参数类型相同, 应该使用 `AnyStr `_. + +.. _typing_imports: + +**导入类型** + + 为了静态分析和类型检查而导入 ``typing`` 和 ``collections.abc`` 模块中的符号时, 一定要导入符号本身. 这样常用的类型注解更简洁, 也符合全世界的习惯. 特别地, 你可以在一行内从 ``typing`` 和 ``collections.abc`` 模块中导入多个特定的类, 例如: .. code-block:: python - from typing import Text - ... - def py2_compatible(x: Text) -> Text: - ... - def py3_only(x: str) -> str: - ... - - 若类型既可以是二进制也可以是文本,那么就使用 ``Union`` 进行注解,并按照之前规则使用合适的文本类型注释. - - .. code-block:: python - - from typing import Text, Union - ... - def py2_compatible(x: Union[bytes, Text]) -> Union[bytes, Text]: - ... - def py3_only(x: Union[bytes, str]) -> Union[bytes, str]: - ... - - 若一个函数中的字符串类型始终相同,比如上述函数中返回值类型和形参类型都一样,使用 `AnyStr `_. - 这样写可以方便将代码移植到 python3 - -**类型的导入** - - 对于 ``typing`` 模块中类的导入,请直接导入类本身.你可以显式的在一行中从 ``typing`` 模块导入多个特定的类,例如: - - .. code-block:: python - - from typing import Any, Dict, Optional + from collections.abc import Mapping, Sequence + from typing import Any, Generic - 以此方式导入的类将被加入到本地的命名空间,因此所有 ``typing`` 模块中的类都应被视为关键字,不要在代码中定义并覆盖它们.若这些类和现行代码中的变量或者方法发生命名冲突,可以考虑使用 ``import x as y``的导入形式: + 采用这种方法时, 导入的类会进入本地命名空间, 因此所有 ``typing`` 和 ``collections.abc`` 模块中的名称都应该和关键词 (keyword) 同等对待. 你不能在自己的代码中定义相同的名字, 无论你是否采用类型注解. 若类型名和某模块中已有的名称出现冲突, 可以用 ``import x as y`` 的导入形式: .. code-block:: python from typing import Any as AnyType -**条件导入** + 只要可行, 就使用内置类型. 利用 Python 3.9 引入的 `PEP-585 `_, 可以在类型注解中使用参数化的容器类型. - 在一些特殊情况下,比如当在运行时需要避免类型检查所需的一些导入时,可能会用到条件导入.但这类方法并不推荐,首选方法应是重构代码使类型检查所需的模块可以在顶层导入. - 仅用于类型注解的导入可以放在 ``if TYPE_CHECKING:`` 语句块内. + .. code-block:: python - #. 通过条件导入引入的类的注解须是字符串string,这样才能和python3.6之前的代码兼容.因为python3.6之前,类型注解是会进行求值的. - #. 条件导入引入的包应仅仅用于类型注解,别名也是如此.否则,将引起运行错误,条件导入的包在运行时是不会被实际导入的. - #. 条件导入的语句块应放在所有常规导入的语句块之后. - #. 在条件导入的语句块的导入语句之间不应有空行. - #. 和常规导入一样,请对该导入语句进行排序. + def generate_foo_scores(foo: set[str]) -> list[float]: + ... + + 注意: `Apache Beam `_ 的用户应该继续导入 ``typing`` 模块提供的参数化容器类型. + + .. code-block:: python + + from typing import Set, List + + # 只有在你使用了 Apache Beam 这样没有为 PEP 585 更新的代码, 或者你的 + # 代码需要在 Python 3.9 以下版本中运行时, 才能使用这种旧风格. + def generate_foo_scores(foo: Set[str]) -> List[float]: + ... + +**有条件的导入** + + 仅在一些特殊情况下, 比如必须在运行时避免导入类型检查所需的模块时, 才能有条件地导入. 不推荐这种写法. 替代方案是重构代码, 使类型检查所需的模块可以在顶层导入. + + 可以把仅用于类型注解的导入放在 ``if TYPE_CHECKING:`` 语句块内. + + #. 在类型注解中, 有条件地导入的类型必须用字符串表示, 这样才能和 Python 3.6 之前的代码兼容. 因为 Python 3.6 之前真的会对类型注解求值. + #. 只有那些仅仅用于类型注解的实例才能有条件地导入, 别名也是如此. 否则会引发运行时错误, 因为运行时不会导入这些模块. + #. 有条件的导入语句应紧随所有常规导入语句之后. + #. 有条件的导入语句之间不能有空行. + #. 和常规导入一样, 请对有条件的导入语句排序. .. code-block:: python @@ -1177,46 +1395,52 @@ Main **循环依赖** - 由类型注释引起的循环依赖可能会导致代码异味,应对其进行重构.虽然从技术上我们可以兼容循环依赖,但是 `构建系统 `_ 是不会容忍这样做的,因为每个模块都需要依赖一个其他模块. - 将引起循环依赖的导入模块使用 ``Any`` 导入.使用 ``alias`` 来起一个有意义的别名,推荐使用真正模块的类型名的字符串作为别名(Any的任何属性依然是Any,使用字符串只是帮助我们理解代码).别名的定义应该和最后的导入语句之间空一行. + 若类型注解引发了循环依赖, 说明代码可能存在问题. 这样的代码适合重构. 虽然技术上我们可以支持循环依赖, 但是很多构建系统 (build system) 不支持. + + 可以用 ``Any`` 替换引起循环依赖的模块. 起一个有意义的别名, 然后使用模块中的真实类型名 (Any 的任何属性依然是 Any). 定义别名的语句应该和最后一行导入语句之间间隔一行. .. code-block:: python from typing import Any - some_mod = Any # some_mod.py imports this module. + some_mod = Any # 因为 some_mod.py 导入了我们的模块. ... def my_method(self, var: "some_mod.SomeType") -> None: - ... + ... -**泛型** +**泛型 (generics)** - 在注释时,尽量将泛型类型注释为类型参数.否则, `泛型参数将被视为是 Any `_ . + 在注解类型时, 尽量为泛型类型填入类型参数. 否则, `泛型参数默认为 Any `_ . + + 正确: .. code-block:: python - def get_names(employee_ids: List[int]) -> Dict[int, Any]: - ... + def get_names(employee_ids: Sequence[int]) -> Mapping[int, str]: + ... + + 错误: .. code-block:: python - # These are both interpreted as get_names(employee_ids: List[Any]) -> Dict[Any, Any] - def get_names(employee_ids: list) -> Dict: - ... + # 这表示 get_names(employee_ids: Sequence[Any]) -> Mapping[Any, Any] + def get_names(employee_ids: Sequence) -> Mapping: + ... - def get_names(employee_ids: List) -> Dict: - ... + 如果泛型类型的参数的确应该是 ``Any``, 请显式地标注, 不过注意 ``TypeVar`` 很可能更合适. - 若实在要用 Any 作为泛型类型,请显式的使用它.但在多数情况下, ``TypeVar`` 通常可能是更好的选择. + 错误: .. code-block:: python - def get_names(employee_ids: List[Any]) -> Dict[Any, Text]: - """Returns a mapping from employee ID to employee name for given IDs.""" + def get_names(employee_ids: Sequence[Any]) -> Mapping[Any, str]: + """返回员工ID到员工名的映射.""" + + 正确: .. code-block:: python - T = TypeVar('T') - def get_names(employee_ids: List[T]) -> Dict[T, Text]: - """Returns a mapping from employee ID to employee name for given IDs.""" + _T = TypeVar('_T') + def get_names(employee_ids: Sequence[_T]) -> Mapping[_T, str]: + """返回员工ID到员工名的映射."""