自动摘要: Python风格指南 扉页 版本:V0.1 作者:朱远翔 参考: [GoogleStyleGuide](https://github.com/google/style ……..
Python 风格指南
扉页
版本:V0.1
作者:朱远翔
参考:
- Google Style Guide
- Google 开源项目风格指南-中文版
背景
Python 是 AI算法组使用的主要脚本语言。这本风格指南主要包含的是针对python的编程准则。
许多团队使用 yapf 作为自动格式化工具以避免格式不一致。
Python语言规范
Lint
使用该 pylintrc 对你的代码运行pylint
定义:
pylint是一个在Python源代码中查找bug的工具. 对于C和C++这样的不那么动态的(译者注: 原文是less dynamic)语言, 这些bug通常由编译器来捕获. 由于Python的动态特性, 有些警告可能不对. 不过伪告警应该很少.
优点:
可以捕获容易忽视的错误, 例如输入错误, 使用未赋值的变量等.
缺点:
pylint不完美. 要利用其优势, 我们有时侯需要: a) 围绕着它来写代码 b) 抑制其告警 c) 改进它, 或者d) 忽略它.
结论:
确保对你的代码运行pylint.
抑制不准确的警告,以便能够将其他警告暴露出来。你可以通过设置一个行注释来抑制警告. 例如:
1 | dict = 'something awful' # Bad Idea... pylint: disable=redefined-builtin |
pylint警告是以符号名(如 empty-docstring
)来标识的。
如果警告的符号名不够见名知意,那么请对其增加一个详细解释。
采用这种抑制方式的好处是我们可以轻松查找抑制并回顾它们.
你可以使用命令 pylint --list-msgs
来获取 pylint 告警列表. 你可以使用命令 pylint --help-msg=C6409
, 以获取关于特定消息的更多信息.
相比较于之前使用的 pylint: disable-msg
, 本文推荐使用 pylint: disable
.
在函数体中 del
未使用的变量可以消除参数未使用告警.记得要加一条注释说明你为何 del
它们,注释使用”Unused”就可以,例如:
1 | def viking_cafe_order(spam, beans, eggs=None): |
其他消除这个告警的方法还有使用_
标志未使用参数,或者给这些参数名加上前缀 unused_
, 或者直接把它们绑定到 _
.但这些方法都不推荐.
导入
仅对包和模块使用导入,而不单独导入函数或者类。typing
模块例外。
定义:
模块间共享代码的重用机制.
优点:
命名空间管理约定十分简单. 每个标识符的源都用一种一致的方式指示. x.Obj表示Obj对象定义在模块x中.
缺点:
模块名仍可能冲突. 有些模块名太长, 不太方便.
结论:
- 使用
import x
来导入包和模块. - 使用
from x import y
, 其中x是包前缀, y是不带前缀的模块名. - 使用
from x import y as z
, 如果两个要导入的模块都叫做y或者y太长了. - 仅当缩写
z
是通用缩写时才可使用import y as z
.(比如np
代表numpy
.)
例如, 模块 sound.effects.echo
可以用如下方式导入:
1 | from sound.effects import echo |
导入时不要使用相对名称. 即使模块在同一个包中, 也要使用完整包名. 这能帮助你避免无意间导入一个包两次.
导入 typing
和 six.moves
模块时可以例外.
包
使用模块的全路径名来导入每个模块
优点:
避免模块名冲突或是因非预期的模块搜索路径导致导入错误. 查找包更容易.
缺点:
部署代码变难, 因为你必须复制包层次.
结论:
所有的新代码都应该用完整包名来导入每个模块.
应该像下面这样导入:
1 | # Yes: |
不应假定主入口脚本所在的目录就在 sys.path
中,虽然这种情况是存在的。当主入口脚本所在目录不在 sys.path
中时,代码将假设 import jodie
是导入的一个第三方库或者是一个名为 jodie
的顶层包,而不是本地的 jodie.py
异常
允许使用异常, 但必须小心
定义:
异常是一种跳出代码块的正常控制流来处理错误或者其它异常条件的方式.
优点:
正常操作代码的控制流不会和错误处理代码混在一起. 当某种条件发生时, 它也允许控制流跳过多个框架. 例如, 一步跳出N个嵌套的函数, 而不必继续执行错误的代码.
缺点:
可能会导致让人困惑的控制流. 调用库时容易错过错误情况.
结论:
异常必须遵守特定条件:
优先合理的使用内置异常类.比如 ValueError
指示了一个程序错误, 比如在方法需要正数的情况下传递了一个负数错误.不要使用 assert
语句来验证公共API的参数值. assert
是用来保证内部正确性的,而不是用来强制纠正参数使用.若需要使用异常来指示某些意外情况,不要用 assert
,用 raise
语句,例如:
1 | # Yes: |
- 模块或包应该定义自己的特定域的异常基类, 这个基类应该从内建的Exception类继承. 模块的异常基类后缀应该叫做
Error
. - 永远不要使用
except:
语句来捕获所有异常, 也不要捕获Exception
或者StandardError
, 除非你打算重新触发该异常, 或者你已经在当前线程的最外层(记得还是要打印一条错误消息). 在异常这方面, Python非常宽容,except:
真的会捕获包括Python语法错误在内的任何错误. 使用except:
很容易隐藏真正的bug. - 尽量减少try/except块中的代码量. try块的体积越大, 期望之外的异常就越容易被触发. 这种情况下, try/except块将隐藏真正的错误.
- 使用finally子句来执行那些无论try块中有没有异常都应该被执行的代码. 这对于清理资源常常很有用, 例如关闭文件.
全局变量
避免全局变量
定义:
定义在模块级的变量.
优点:
偶尔有用.
缺点:
导入时可能改变模块行为, 因为导入模块时会对模块级变量赋值.
结论:
避免使用全局变量.
鼓励使用模块级的常量,例如 MAX_HOLY_HANDGRENADE_COUNT = 3
.注意常量命名必须全部大写,用 _
分隔.具体参见 命名规则
若必须要使用全局变量,应在模块内声明全局变量,并在名称前 _
使之成为模块内部变量.外部访问必须通过模块级的公共函数.具体参见 命名规则
嵌套/局部/内部类或函数
使用内部类或者嵌套函数可以用来覆盖某些局部变量.
定义:
类可以定义在方法, 函数或者类中. 函数可以定义在方法或函数中. 封闭区间中定义的变量对嵌套函数是只读的. (译者注:即内嵌函数可以读外部函数中定义的变量,但是无法改写,除非使用 nonlocal
)
优点:
允许定义仅用于有效范围的工具类和函数.在装饰器中比较常用.
缺点:
嵌套类或局部类的实例不能序列化(pickled). 内嵌的函数和类无法直接测试.同时内嵌函数和类会使外部函数的可读性变差.
结论:
使用内部类或者内嵌函数可以忽视一些警告.但是应该避免使用内嵌函数或类,除非是想覆盖某些值.若想对模块的用户隐藏某个函数,不要采用嵌套它来隐藏,应该在需要被隐藏的方法的模块级名称加 _
前缀,这样它依然是可以被测试的.
推导式&生成式
可以在简单情况下使用
定义:
列表,字典和集合的推导&生成式提供了一种简洁高效的方式来创建容器和迭代器, 而不必借助map()
, filter()
, 或者lambda
.(译者注: 元组是没有推导式的, ()
内加类似推导式的句式返回的是个生成器)
优点:
简单的列表推导可以比其它的列表创建方法更加清晰简单. 生成器表达式可以十分高效, 因为它们避免了创建整个列表.
缺点:
复杂的列表推导或者生成器表达式可能难以阅读.
结论:
适用于简单情况. 每个部分应该单独置于一行: 映射表达式, for语句, 过滤器表达式. 禁止多重for语句或过滤器表达式. 复杂情况下还是使用循环.
1 | # Yes: |
默认迭代器和操作符
如果类型支持, 就使用默认迭代器和操作符. 比如列表, 字典及文件等.
定义:
容器类型, 像字典和列表, 定义了默认的迭代器和关系测试操作符(in和not in)
优点:
默认操作符和迭代器简单高效, 它们直接表达了操作, 没有额外的方法调用. 使用默认操作符的函数是通用的. 它可以用于支持该操作的任何类型.
缺点:
你没法通过阅读方法名来区分对象的类型(例如, has_key()意味着字典). 不过这也是优点.
结论:
如果类型支持, 就使用默认迭代器和操作符, 例如列表, 字典和文件. 内建类型也定义了迭代器方法. 优先考虑这些方法, 而不是那些返回列表的方法. 当然,这样遍历容器时,你将不能修改容器. 除非必要,否则不要使用诸如 dict.iter*()
这类python2的特定迭代方法.
1 | # Yes: |
生成器
按需使用生成器.(译者注: 参看:注释)
定义:
所谓生成器函数, 就是每当它执行一次生成(yield)语句, 它就返回一个迭代器, 这个迭代器生成一个值. 生成值后, 生成器函数的运行状态将被挂起, 直到下一次生成.
优点:
简化代码, 因为每次调用时, 局部变量和控制流的状态都会被保存. 比起一次创建一系列值的函数, 生成器使用的内存更少.
缺点:
没有.
结论:
鼓励使用. 注意在生成器函数的文档字符串中使用”Yields:”而不是”Returns:”.
Lambda函数
适用于单行函数。对于常见的操作符,例如乘法操作符,使用 operator
模块中的函数以代替lambda函数. 例如, 推荐使用 operator.mul
, 而不是 lambda x, y: x * y
.
定义:
与语句相反, lambda在一个表达式中定义匿名函数. 常用于为 map()
和 filter()
之类的高阶函数定义回调函数或者操作符.
优点:
方便.
缺点:
比本地函数更难阅读和调试. 没有函数名意味着堆栈跟踪更难理解. 由于lambda函数通常只包含一个表达式, 因此其表达能力有限.
结论:
适用于单行函数. 如果代码超过60-80个字符, 最好还是定义成常规(嵌套)函数.
条件表达式
适用于单行函数
定义:
条件表达式(又名三元运算符)是对于if语句的一种更为简短的句法规则. 例如: x = 1 if cond else 2
.
优点:
比if语句更加简短和方便.
缺点:
比if语句难于阅读. 如果表达式很长, 难于定位条件.
结论:
适用于单行函数. 写法上推荐真实表达式,if表达式,else表达式每个独占一行.在其他情况下,推荐使用完整的if语句.
1 | # Yes: |
默认参数值
适用于大部分情况.
定义:
你可以在函数参数列表的最后指定变量的值, 例如, def foo(a, b = 0):
. 如果调用foo时只带一个参数, 则b被设为0. 如果带两个参数, 则b的值等于第二个参数.
优点:
你经常会碰到一些使用大量默认值的函数, 但偶尔(比较少见)你想要覆盖这些默认值. 默认参数值提供了一种简单的方法来完成这件事, 你不需要为这些罕见的例外定义大量函数. 同时, Python也不支持重载方法和函数, 默认参数是一种”仿造”重载行为的简单方式.
缺点:
默认参数只在模块加载时求值一次. 如果参数是列表或字典之类的可变类型, 这可能会导致问题. 如果函数修改了对象(例如向列表追加项), 默认值就被修改了.
结论:
鼓励使用, 不过有如下注意事项:
不要在函数或方法定义中使用可变对象作为默认值.
1 | #Yes: |
特性(properties)
(译者注:参照fluent python.这里将 “property” 译为”特性”,而 “attribute” 译为属性. python中数据的属性和处理数据的方法统称属性”(attribute)”, 而在不改变类接口的前提下用来修改数据属性的存取方法我们称为”特性(property)”.)
访问和设置数据成员时, 你通常会使用简单, 轻量级的访问和设置函数.建议使用特性(properties)来代替它们.
定义:
一种用于包装方法调用的方式. 当运算量不大, 它是获取和设置属性(attribute)的标准方式.
优点:
通过消除简单的属性(attribute)访问时显式的get和set方法调用, 可读性提高了. 允许懒惰的计算. 用Pythonic的方式来维护类的接口. 就性能而言, 当直接访问变量是合理的, 添加访问方法就显得琐碎而无意义. 使用特性(properties)可以绕过这个问题. 将来也可以在不破坏接口的情况下将访问方法加上.
缺点:
特性(properties)是在get和set方法声明后指定, 这需要使用者在接下来的代码中注意: set和get是用于特性(properties)的(除了用 @property
装饰器创建的只读属性). 必须继承自object类. 可能隐藏比如操作符重载之类的副作用. 继承时可能会让人困惑.
(译者注:这里没有修改原始翻译,其实就是 @property 装饰器是不会被继承的)
结论:
你通常习惯于使用访问或设置方法来访问或设置数据, 它们简单而轻量. 不过我们建议你在新的代码中使用属性. 只读属性应该用 @property
装饰器 来创建.
如果子类没有覆盖属性, 那么属性的继承可能看上去不明显. 因此使用者必须确保访问方法间接被调用, 以保证子类中的重载方法被属性调用(使用模板方法设计模式).
1 | # Yes: |
(译者注: 老实说, 我觉得这段示例代码很不恰当, 有必要这么蛋疼吗?)
True/False的求值
尽可能使用隐式false
定义:
Python在布尔上下文中会将某些值求值为false. 按简单的直觉来讲, 就是所有的”空”值都被认为是false. 因此0, None, [], {}, “” 都被认为是false.
优点:
使用Python布尔值的条件语句更易读也更不易犯错. 大部分情况下, 也更快.
缺点:
对C/C++开发人员来说, 可能看起来有点怪.
结论:
尽可能使用隐式的false, 例如: 使用 if foo:
而不是 if foo != []:
. 不过还是有一些注意事项需要你铭记在心:
- 对于
None
等单例对象测试时,使用is
或者is not
.当你要测试一个默认值是None的变量或参数是否被设为其它值. 这个值在布尔语义下可能是false!
(译者注:is
比较的是对象的id(), 这个函数返回的通常是对象的内存地址,考虑到CPython的对象重用机制,可能会出现生命周不重叠的两个对象会有相同的id) - 永远不要用==将一个布尔量与false相比较. 使用
if not x:
代替. 如果你需要区分false和None, 你应该用像if not x and x is not None:
这样的语句. - 对于序列(字符串, 列表, 元组), 要注意空序列是false. 因此
if not seq:
或者if seq:
比if len(seq):
或if not len(seq):
要更好. - 处理整数时, 使用隐式false可能会得不偿失(即不小心将None当做0来处理). 你可以将一个已知是整型(且不是len()的返回结果)的值与0比较.
1 | #Yes: |
- 注意
"0"
(字符串)会被当做true.
过时的语言特性
尽可能使用字符串方法取代字符串模块. 使用函数调用语法取代apply()
. 使用列表推导, for
循环取代filter()
, map()
以及reduce()
.
定义:
当前版本的Python提供了大家通常更喜欢的替代品.
结论:
我们不使用不支持这些特性的Python版本, 所以没理由不用新的方式.
1 | # Yes: |
词法作用域(Lexical Scoping)
推荐使用
定义:
嵌套的Python函数可以引用外层函数中定义的变量, 但是不能够对它们赋值. 变量绑定的解析是使用词法作用域, 也就是基于静态的程序文本. 对一个块中的某个名称的任何赋值都会导致Python将对该名称的全部引用当做局部变量, 甚至是赋值前的处理. 如果碰到global声明, 该名称就会被视作全局变量.
一个使用这个特性的例子:
1 | def get_adder(summand1): |
(译者注: 这个例子有点诡异, 你应该这样使用这个函数: sum = get_adder(summand1)(summand2)
)
优点:
通常可以带来更加清晰, 优雅的代码. 尤其会让有经验的Lisp和Scheme(还有Haskell, ML等)程序员感到欣慰.
缺点:
可能导致让人迷惑的bug. 例如下面这个依据 PEP-0227 的例子:
1 | i = 4 |
因此 foo([1, 2, 3])
会打印 1 2 3 3
, 不是 1 2 3 4
.
(译者注: x是一个列表, for循环其实是将x中的值依次赋给i.这样对i的赋值就隐式的发生了, 整个foo函数体中的i都会被当做局部变量, 包括bar()中的那个. 这一点与C++之类的静态语言还是有很大差别的.)
结论:
鼓励使用.
函数与方法装饰器
如果好处很显然, 就明智而谨慎的使用装饰器,避免使用 staticmethod
以及谨慎使用classmethod
.
定义:
用于函数及方法的装饰器 (也就是@标记). 最常见的装饰器是@classmethod
和 @staticmethod
, 用于将常规函数转换成类方法或静态方法. 不过, 装饰器语法也允许用户自定义装饰器. 特别地, 对于某个函数 my_decorator
, 下面的两段代码是等效的:
1 | class C(object): |
优点:
优雅的在函数上指定一些转换. 该转换可能减少一些重复代码, 保持已有函数不变(enforce invariants), 等.
缺点:
装饰器可以在函数的参数或返回值上执行任何操作, 这可能导致让人惊异的隐藏行为. 而且, 装饰器在导入时执行. 从装饰器代码中捕获错误并处理是很困难的.
结论:
如果好处很显然, 就明智而谨慎的使用装饰器. 装饰器应该遵守和函数一样的导入和命名规则. 装饰器的python文档应该清晰的说明该函数是一个装饰器. 请为装饰器编写单元测试.
- 避免装饰器自身对外界的依赖(即不要依赖于文件, socket, 数据库连接等), 因为装饰器运行时这些资源可能不可用(由
pydoc
或其它工具导入). 应该保证一个用有效参数调用的装饰器在所有情况下都是成功的. - 装饰器是一种特殊形式的”顶级代码”. 参考后面关于
Main <main>
的话题. - 除非是为了将方法和现有的API集成,否则不要使用
staticmethod
.多数情况下,将方法封装成模块级的函数可以达到同样的效果. - 谨慎使用
classmethod
.通常只在定义备选构造函数,或者写用于修改诸如进程级缓存等必要的全局状态的特定类方法才用。
线程
不要依赖内建类型的原子性.
虽然Python的内建类型例如字典看上去拥有原子操作, 但是在某些情形下它们仍然不是原子的(即: 如果hash或eq被实现为Python方法)且它们的原子性是靠不住的. 你也不能指望原子变量赋值(因为这个反过来依赖字典).
优先使用Queue模块的 Queue
数据类型作为线程间的数据通信方式. 另外, 使用threading模块及其锁原语(locking primitives). 了解条件变量的合适使用方式, 这样你就可以使用 threading.Condition
来取代低级别的锁了.
威力过大的特性
避免使用这些特性
定义:
Python是一种异常灵活的语言, 它为你提供了很多花哨的特性, 诸如元类(metaclasses), 字节码访问, 任意编译(on-the-fly compilation), 动态继承, 对象父类重定义(object reparenting), 导入黑客(import hacks), 反射, 系统内修改(modification of system internals), 等等.
优点:
强大的语言特性, 能让你的代码更紧凑.
缺点:
使用这些很”酷”的特性十分诱人, 但不是绝对必要. 使用奇技淫巧的代码将更加难以阅读和调试. 开始可能还好(对原作者而言), 但当你回顾代码, 它们可能会比那些稍长一点但是很直接的代码更加难以理解.
结论:
在你的代码中避免这些特性.
当然,利用了这些特性的来编写的一些标准库是值得去使用的,比如 abc.ABCMeta
, collection.namedtuple
, dataclasses
, enum
等.
现代python: python3 和 from __future__ imports
尽量使用 python3, 即使使用非 python3 写的代码.也应该尽量兼容.
定义:
python3 是 python 的一个重大变化,虽然已有大量代码是 python2.7 写的,但是通过一些简单的调整,就可以使之在 python3 下运行.
优点:
只要确定好项目的所有依赖,那么用 python3 写代码可以更加清晰和方便运行.
缺点:
导入一些看上去实际用不到的模块到代码里显得有些奇葩.
结论:from __future__ imports
鼓励使用 from __future__ import
语句,所有的新代码都应该包含以下内容,并尽可能的与之兼容:
1 | from __future__ import absolute_import |
以上导入的详情参见 absolute imports, division behavior, print function
除非代码是只在python3下运行,否则不要删除以上导入.最好在所有文件里都保留这样的导入,这样若有人用到了这些方法时,编辑时不会忘记导入.
还有其他的一些来自 from __future__
的语句.请在你认为合适的地方使用它们.本文没有推荐 unicode_literals
,因为我们认为它不是很棒的改进,它在 python2.7 中大量引入例隐式的默认编码转换.大多数情况下还是推荐显式的使用 b
和 u
以及 unicode字符串来显式的指示编码转换.
six,future,past
当项目需要同时支持 python2 和 python3 时,请根据需要使用 six, future, past. 这些库可以使代码更加清晰和简单.
代码类型注释
你可以根据 PEP-484 来对 python3 代码进行注释,并使用诸如 pytype 之类的类型检查工具来检查代码.
类型注释既可以写在源码,也可以写在 pyi 中.推荐尽量写在源码里,对于第三方扩展包,可以写在pyi文件里.
定义:
用于函数参数和返回值的类型注释:
1 | def func(a: int) -> List[int]: |
也可以使用 PEP-526 中的语法来声明变量类型:
1 | a: SomeType = some_func() |
在必须支持老版本 python 运行的代码中则可以这样注释:
1 | a = some_func() #type: SomeType |
优点:
可以提高代码可读性和可维护性.同时一些类型检查器可以帮您提早发现一些运行时错误,并降低您使用大威力特性的必要.
缺点:
必须时常更新类型声明.过时的类型声明可能会误导您.使用类型检查器会抑制您使用大威力特性.
结论:
强烈推荐您在更新代码时使用 python 类型分析.在添加或修改公共API时使用类型注释,在最终构建整个项目前使用 pytype 来进行检查.由于静态分析对于 python 来说还不够成熟,因此可能会出现一些副作用(例如错误推断的类型)可能会阻碍项目的部署.在这种情况下,建议作者添加一个 TODO 注释或者链接,来描述当前构建文件或是代码本身中使用类型注释导致的问题.
(译者注: 代码类型注释在帮助IDE或是vim等进行补全倒是很有效)
Python风格规范
分号
不要在行尾加分号, 也不要用分号将两条命令放在同一行.
行长度
每行不超过80个字符
例外:
长的导入模块语句
注释里的URL,路径以及其他的一些长标记
不便于换行,不包含空格的模块级字符串常量,比如url或者路径
- Pylint 禁用注释.(例如:`# pylint: disable=invalid-name)
除非是在 with
语句需要三个以上的上下文管理器的情况下,否则不要使用反斜杠连接行.
Python会将 圆括号, 中括号和花括号中的行隐式的连接起来 , 你可以利用这个特点. 如果需要, 你可以在表达式外围增加一对额外的圆括号.
1 | # Yes: |
如果一个文本字符串在一行放不下, 可以使用圆括号来实现隐式行连接:
1 | x = ('This will build a very long long ' |
在注释中,如果必要,将长的URL放在一行上。
1 | # Yes: |
当 with
表达式需要使用三个及其以上的上下文管理器时,可以使用反斜杠换行.若只需要两个,请使用嵌套的with.
1 | # Yes: |
注意上面例子中的元素缩进; 你可以在本文的 [缩进] 部分找到解释.
另外在其他所有情况下,若一行超过80个字符,但 yapf 却无法将该行字数降至80个字符以下时,则允许该行超过80个字符长度.
括号
宁缺毋滥的使用括号
除非是用于实现行连接, 否则不要在返回语句或条件语句中使用括号. 不过在元组两边使用括号是可以的.
1 | # Yes: |
缩进
用4个空格来缩进代码
绝对不要用tab, 也不要tab和空格混用. 对于行连接的情况, 你应该要么垂直对齐换行的元素(见 [行长度] 部分的示例), 或者使用4空格的悬挂式缩进(这时第一行不应该有参数):
1 | # Yes: |
序列元素尾部逗号
仅当 ]
, )
, }
和末位元素不在同一行时,推荐使用序列元素尾部逗号. 当末位元素尾部有逗号时,元素后的逗号可以指示 YAPF 将序列格式化为每行一项.
1 | # Yes: |
空行
顶级定义之间空两行, 方法定义之间空一行
顶级定义之间空两行, 比如函数或者类定义. 方法定义, 类定义与第一个方法之间, 都应该空一行. 函数或方法中, 某些地方要是你觉得合适, 就空一行.
空格
按照标准的排版规范来使用标点两边的空格
括号内不要有空格.
1 | # Yes: |
不要在逗号, 分号, 冒号前面加空格, 但应该在它们后面加(除了在行尾).
1 | # Yes: |
参数列表, 索引或切片的左括号前不应加空格.
1 | # Yes: |
在二元操作符两边都加上一个空格, 比如赋值(=), 比较(==, <, >, !=, <>, <=, >=, in, not in, is, is not), 布尔(and, or, not). 至于算术操作符两边的空格该如何使用, 需要你自己好好判断. 不过两侧务必要保持一致.
1 | # Yes: |
当 =
用于指示关键字参数或默认参数值时, 不要在其两侧使用空格. 但若存在类型注释的时候,需要在 =
周围使用空格.
1 | # Yes: |
不要用空格来垂直对齐多行间的标记, 因为这会成为维护的负担(适用于:, #, =等):
1 | # Yes: |
Shebang
大部分.py文件不必以#!作为文件的开始. 根据 PEP-394, 程序的main文件应该以 #!/usr/bin/python2
或者 #!/usr/bin/python3
开始.
(译者注: 在计算机科学中, Shebang_ (也称为Hashbang)是一个由井号和叹号构成的字符串行(#!), 其出现在文本文件的第一行的前两个字符. 在文件中存在Shebang的情况下, 类Unix操作系统的程序载入器会分析Shebang后的内容, 将这些内容作为解释器指令, 并调用该指令, 并将载有Shebang的文件路径作为该解释器的参数. 例如, 以指令#!/bin/sh开头的文件在执行时会实际调用/bin/sh程序.)
#!
先用于帮助内核找到Python解释器, 但是在导入模块时, 将会被忽略. 因此只有被直接执行的文件中才有必要加入 #!
.
注释
确保对模块, 函数, 方法和行内注释使用正确的风格
文档字符串
Python有一种独一无二的的注释方式: 使用文档字符串. 文档字符串是包, 模块, 类或函数里的第一个语句. 这些字符串可以通过对象的 __doc__
成员被自动提取, 并且被pydoc所用. (你可以在你的模块上运行pydoc试一把, 看看它长什么样). 我们对文档字符串的惯例是使用三重双引号”””( PEP-257 ). 一个文档字符串应该这样组织: 首先是一行以句号, 问号或惊叹号结尾的概述(或者该文档字符串单纯只有一行). 接着是一个空行. 接着是文档字符串剩下的部分, 它应该与文档字符串的第一行的第一个引号对齐. 下面有更多文档字符串的格式化规范.
模块
每个文件应该包含一个许可样板. 根据项目使用的许可(例如, Apache 2.0, BSD, LGPL, GPL), 选择合适的样板.
其开头应是对模块内容和用法的描述.
1 | """A one line summary of the module or program, terminated by a period. |
函数和方法
下文所指的函数,包括函数, 方法, 以及生成器.
一个函数必须要有文档字符串, 除非它满足以下条件:
- 外部不可见
- 非常短小
- 简单明了
文档字符串应该包含函数做什么, 以及输入和输出的详细描述. 通常, 不应该描述”怎么做”, 除非是一些复杂的算法. 文档字符串应该提供足够的信息, 当别人编写代码调用该函数时, 他不需要看一行代码, 只要看文档字符串就可以了. 对于复杂的代码, 在代码旁边加注释会比使用文档字符串更有意义.
覆盖基类的子类方法应有一个类似 See base class
的简单注释来指引读者到基类方法的文档注释.若重载的子类方法和基类方法有很大不同,那么注释中应该指明这些信息.
关于函数的几个方面应该在特定的小节中进行描述记录, 这几个方面如下文所述. 每节应该以一个标题行开始. 标题行以冒号结尾. 除标题行外, 节的其他内容应被缩进2个空格.
Args:
列出每个参数的名字, 并在名字后使用一个冒号和一个空格, 分隔对该参数的描述.如果描述太长超过了单行80字符,使用2或者4个空格的悬挂缩进(与文件其他部分保持一致).
描述应该包括所需的类型和含义.
如果一个函数接受_foo(可变长度参数列表)或者bar (任意关键字参数), 应该详细列出foo和_bar.
Returns: (或者 Yields: 用于生成器)
描述返回值的类型和语义. 如果函数返回None, 这一部分可以省略.
Raises:
列出与接口有关的所有异常.
1 | def fetch_smalltable_rows(table_handle: smalltable.Table, |
在 Args:
上进行换行也是可以的:
1 | def fetch_smalltable_rows(table_handle: smalltable.Table, |
类
类应该在其定义下有一个用于描述该类的文档字符串. 如果你的类有公共属性(Attributes), 那么文档中应该有一个属性(Attributes)段. 并且应该遵守和函数参数相同的格式.
1 | class SampleClass(object): |
块注释和行注释
最需要写注释的是代码中那些技巧性的部分. 如果你在下次 代码审查 的时候必须解释一下, 那么你应该现在就给它写注释. 对于复杂的操作, 应该在其操作开始前写上若干行注释. 对于不是一目了然的代码, 应在其行尾添加注释.
1 | # We use a weighted dictionary search to find out where i is in |
为了提高可读性, 注释应该至少离开代码2个空格.
另一方面, 绝不要描述代码. 假设阅读代码的人比你更懂Python, 他只是不知道你的代码要做什么.
1 | # BAD COMMENT: Now go through the b array and make sure whenever i occurs |
标点符号,拼写和语法
注意标点符号,拼写和语法
注释应有适当的大写和标点,句子应该尽量完整.对于诸如在行尾上的较短注释,可以不那么正式,但是也应该尽量保持风格抑制.
类
如果一个类不继承自其它类, 就显式的从object继承. 嵌套类也一样.(除非是为了和 python2 兼容)
Yes:
1 | class SampleClass(object): |
No:
1 | class SampleClass: |
继承自 object
是为了使属性(properties)正常工作, 并且这样可以保护你的代码, 使其不受 PEP-3000 的一个特殊的潜在不兼容性影响. 这样做也定义了一些特殊的方法, 这些方法实现了对象的默认语义, 包括 __new__, __init__, __delattr__, __getattribute__, __setattr__, __hash__, __repr__, and __str__
.
字符串
即使参数都是字符串, 使用%操作符或者格式化方法格式化字符串. 不过也不能一概而论, 你需要在+和%之间好好判定.
1 | # Yes: |
避免在循环中用+和+=操作符来累加字符串. 由于字符串是不可变的, 这样做会创建不必要的临时对象, 并且导致二次方而不是线性的运行时间. 作为替代方案, 你可以将每个子串加入列表, 然后在循环结束后用 .join
连接列表. (也可以将每个子串写入一个 cStringIO.StringIO
缓存中.)
1 | # Yes: |
在同一个文件中, 保持使用字符串引号的一致性. 使用单引号’或者双引号”之一用以引用字符串, 并在同一文件中沿用. 在字符串内可以使用另外一种引号, 以避免在字符串中使用.
1 | # Yes: |
为多行字符串使用三重双引号”””而非三重单引号’’’. 当且仅当项目中使用单引号’来引用字符串时, 才可能会使用三重’’’为非文档字符串的多行字符串来标识引用. 文档字符串必须使用三重双引号”””.
多行字符串不应随着代码其他部分缩进的调整而发生位置移动. 如果需要避免在字符串中嵌入额外的空间,可以使用串联的单行字符串或者使用 textwrap.dedent()
来删除每行多余的空间.
1 | # No: |
文件和sockets
在文件和sockets结束时, 显式的关闭它.
除文件外, sockets或其他类似文件的对象在没有必要的情况下打开, 会有许多副作用, 例如:
- 它们可能会消耗有限的系统资源, 如文件描述符. 如果这些资源在使用后没有及时归还系统, 那么用于处理这些对象的代码会将资源消耗殆尽.
- 持有文件将会阻止对于文件的其他诸如移动、删除之类的操作.
- 仅仅是从逻辑上关闭文件和sockets, 那么它们仍然可能会被其共享的程序在无意中进行读或者写操作. 只有当它们真正被关闭后, 对于它们尝试进行读或者写操作将会抛出异常, 并使得问题快速显现出来.
而且, 幻想当文件对象析构时, 文件和sockets会自动关闭, 试图将文件对象的生命周期和文件的状态绑定在一起的想法, 都是不现实的. 因为有如下原因:
- 没有任何方法可以确保运行环境会真正的执行文件的析构. 不同的Python实现采用不同的内存管理技术, 比如延时垃圾处理机制. 延时垃圾处理机制可能会导致对象生命周期被任意无限制的延长.
- 对于文件意外的引用,会导致对于文件的持有时间超出预期(比如对于异常的跟踪, 包含有全局变量等).
推荐使用 “with”语句 以管理文件:
1 | with open("hello.txt") as hello_file: |
对于不支持使用”with”语句的类似文件的对象,使用 contextlib.closing():
1 | import contextlib |
Legacy AppEngine 中Python 2.5的代码如使用”with”语句, 需要添加 from __future__ import with_statement
.
TODO注释
为临时代码使用TODO注释, 它是一种短期解决方案. 不算完美, 但够好了.
TODO注释应该在所有开头处包含”TODO”字符串, 紧跟着是用括号括起来的你的名字, email地址或其它标识符. 然后是一个可选的冒号. 接着必须有一行注释, 解释要做什么. 主要目的是为了有一个统一的TODO格式, 这样添加注释的人就可以搜索到(并可以按需提供更多细节). 写了TODO注释并不保证写的人会亲自解决问题. 当你写了一个TODO, 请注上你的名字.
1 | # TODO(kl@gmail.com): Use a "*" here for string repetition. |
如果你的TODO是”将来做某事”的形式, 那么请确保你包含了一个指定的日期(“2009年11月解决”)或者一个特定的事件(“等到所有的客户都可以处理XML请求就移除这些代码”).
导入格式
每个导入应该独占一行, typing
的导入除外
1 | # Yes: |
导入总应该放在文件顶部, 位于模块注释和文档字符串之后, 模块全局变量和常量之前. 导入应该按照从最通用到最不通用的顺序分组:
1 | # `__future__` 导入 |
每种分组中, 应该根据每个模块的完整包路径按字典序排序, 忽略大小写.
1 | import collections |
语句
通常每个语句应该独占一行
不过, 如果测试结果与测试语句在一行放得下, 你也可以将它们放在同一行. 如果是if语句, 只有在没有else时才能这样做. 特别地, 绝不要对 try/except
这样做, 因为try和except不能放在同一行.
1 | # Yes: |
访问控制
在Python中, 对于琐碎又不太重要的访问函数, 你应该直接使用公有变量来取代它们, 这样可以避免额外的函数调用开销. 当添加更多功能时, 你可以用属性(property)来保持语法的一致性.
(译者注: 重视封装的面向对象程序员看到这个可能会很反感, 因为他们一直被教育: 所有成员变量都必须是私有的! 其实, 那真的是有点麻烦啊. 试着去接受Pythonic哲学吧)
另一方面, 如果访问更复杂, 或者变量的访问开销很显著, 那么你应该使用像 get_foo()
和 set_foo()
这样的函数调用. 如果之前的代码行为允许通过属性(property)访问 , 那么就不要将新的访问函数与属性绑定. 这样, 任何试图通过老方法访问变量的代码就没法运行, 使用者也就会意识到复杂性发生了变化.
命名
模块名写法: 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
作为文件后缀名,不要用破折号.
应该避免的名称
- 单字符名称, 除了计数器和迭代器,作为
try/except
中异常声明的e
,作为with
语句中文件句柄的f
. - 包/模块名中的连字符(-)
- 双下划线开头并结尾的名称(Python保留, 例如init)
命名约定
- 所谓”内部(Internal)”表示仅模块内可用, 或者, 在类内是保护或私有的.
- 用单下划线(_)开头表示模块变量或函数是protected的(使用from module import *时不会包含).
- 用双下划线(__)开头的实例变量或方法表示类内私有.
- 将相关的类和顶级函数放在同一个模块里. 不像Java, 没必要限制一个类一个模块.
- 对类名使用大写字母开头的单词(如CapWords, 即Pascal风格), 但是模块名应该用小写加下划线的方式(如lower_with_under.py). 尽管已经有很多现存的模块使用类似于CapWords.py这样的命名, 但现在已经不鼓励这样做, 因为如果模块名碰巧和类名一致, 这会让人困扰.
文件名
所有python脚本文件都应该以 .py
为后缀名且不包含 -
.若是需要一个无后缀名的可执行文件,可以使用软联接或者包含 exec "$0.py" "$@"
的bash脚本.
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)__lower_with_under(private) |
Method Names | lower_with_under() | _lower_with_under(protected)__lower_with_under(private) |
Function/Method Parameters | lower_with_under | |
Local Variabels | lower_with_under |
Main
即使是一个打算被用作脚本的文件, 也应该是可导入的. 并且简单的导入不应该导致这个脚本的主功能(main functionality)被执行, 这是一种副作用. 主功能应该放在一个main()函数中.
在Python中, pydoc以及单元测试要求模块必须是可导入的. 你的代码应该在执行主程序前总是检查 if __name__ == '__main__'
, 这样当模块被导入时主程序就不会被执行.
若使用 absl, 请使用 app.run
:
1 | from absl import app |
否则,使用:
1 | def main(): |
所有的顶级代码在模块导入时都会被执行. 要小心不要去调用函数, 创建对象, 或者执行那些不应该在使用pydoc时执行的操作.
函数长度
推荐函数功能尽量集中,简单,小巧
不对函数长度做硬性限制.但是若一个函数超过来40行,推荐考虑一下是否可以在不损害程序结构的情况下对其进行分解.
因为即使现在长函数运行良好,但几个月后可能会有人修改它并添加一些新的行为,这容易产生难以发现的bug.保持函数的简练,使其更加容易阅读和修改.
当遇到一些很长的函数时,若发现调试比较困难或是想在其他地方使用函数的一部分功能,不妨考虑将这个场函数进行拆分.
类型注释
通用规则
请先熟悉下 PEP-484
对于方法,仅在必要时才对
self
或cls
注释若对类型没有任何显示,请使用
Any
无需注释模块中的所有函数
- 公共的API需要注释
- 在代码的安全性,清晰性和灵活性上进行权衡是否注释
- 对于容易出现类型相关的错误的代码进行注释
- 难以理解的代码请进行注释
- 若代码中的类型已经稳定,可以进行注释. 对于一份成熟的代码,多数情况下,即使注释了所有的函数,也不会丧失太多的灵活性.
换行
尽量遵守既定的缩进规则.注释后,很多函数签名将会变成每行一个参数.
1 | def my_method(self, |
尽量在变量之间换行而不是在变量和类型注释之间.当然,若所有东西都在一行上,也可以接受.
1 | def my_method(self, first_var: int) -> int: |
若是函数名,末位形参和返回值的类型注释太长,也可以进行换行,并在新行进行4格缩进.
1 | def my_method( |
若是末位形参和返回值类型注释不适合在同一行上,可以换行,缩进为4空格,并保持闭合的括号 )
和 def
对齐
1 | # Yes: |
pylint
允许闭合括号 ‘)’ 换至新行并与 开启括号 ‘(‘ 对齐,但这样的可读性不好.
1 | # No: |
如上所示,尽量不要在一个类型注释中进行换行.但是有时类型注释过长需要换行时,请尽量保持子类型中不被换行.
1 | def my_method( |
若一个类型注释确实太长,则应优先考虑对过长的类型使用别名 alias. 其次是考虑在冒号后”:”进行换行并添加4格空格缩进.
1 | # Yes: |
预先声明
1 | 若需要使用一个当前模块尚未定义的类名,比如想在类声明中使用类名,请使用类名的字符串 |
参数默认值
依据 PEP-008,仅对同时具有类型注释和默认值的参数的 =
周围加空格.
1 |
|
NoneType
在python的类型系统中, NoneType
是 “一等对象”,为了输入方便, None
是 NoneType
的别名.一个变量若是 None
,则该变量必须被声明.我们可以使用 Union
, 但若类型仅仅只是对应另一个其他类型,建议使用 Optional
.
尽量显式而非隐式的使用 Optional
.在PEP-484的早期版本中允许使用 a: Text = None
来替代 a: Optional[Text] = None
,当然,现在不推荐这么做了.
1 | # Yes: |
类型别名
复杂类型应使用别名,别名的命名可参照帕斯卡命名.若别名仅在当前模块使用,应在名称前加_
变为私有的.
如下例子中,模块名和类型名连一起过长:
1 | _ShortName = module_with_long_name.TypeWithLongName |
忽略类型注释
可以使用特殊的行尾注释 # type: ignore
来禁用该行的类型检查.pytype
针对特定错误有一个禁用选项(类似lint):
1 | # pytype: disable=attribute-error |
变量类型注解
当一个内部变量难以推断其类型时,可以有以下方法来指示其类型:
类型注释
使用行尾注释 # type:
:
1 | a = SomeUndecoratedFunction() # type: Foo |
带类型注解的复制
如函数形参一样,在变量名和等号间加入冒号和类型:
1 | a: Foo = SomeUndecoratedFunction() |
Tuples vs Lists
类型化的Lists只能包含单一类型的元素.但类型化的Tuples可以包含单一类型的元素或者若干个不同类型的元素,通常被用来注解返回值的类型.
(译者注: 注意这里是指的类型注解中的写法,实际python中,list和tuple都是可以在一个序列中包含不同类型元素的,当然,本质其实list和tuple中放的是元素的引用)
1 | a = [1, 2, 3] # type: List[int] |
TypeVars
python的类型系统是支持泛型的.一种常见的方式就是使用工厂函数 TypeVars
.
1 | from typing import List, TypeVar |
TypeVar也可以被限定成若干种类型
1 | AddableType = TypeVar("AddableType", int, float, Text) |
typing
模块中一个常见的预定义类型变量是 AnyStr
.它可以用来注解类似 bytes
, unicode
以及一些相似类型.
1 | from typing import AnyStr |
字符串类型
如何正确的注释字符串的相关类型和要使用的python版本有关.
对于仅在 python3 下运行的代码,首选使用 str
. 使用 Text
也可以.但是两个不要混用,保持风格一致.
对于需要兼容 python2 的代码,使用 Text
.在少数情况下,使用 str
也许更加清晰.不要使用 unicode
,因为 python3 里没有这个类型.
造成这种差异的原因是因为,在不同的python版本中,str
意义不同.
1 | # No: |
对于需要处理二进制数据的代码,使用 bytes
.
1 | def deals_with_binary_data(x: bytes) -> bytes: |
python2 中的文本类数据类型包括str
和unicode
,而python3 中仅有 str
.
1 | from typing import Text |
若类型既可以是二进制也可以是文本,那么就使用 Union
进行注解,并按照之前规则使用合适的文本类型注释.
1 | from typing import Text, Union |
若一个函数中的字符串类型始终相同,比如上述函数中返回值类型和形参类型都一样,使用 AnyStr。
这样写可以方便将代码移植到 python3
类型的导入
对于 typing
模块中类的导入,请直接导入类本身.你可以显式的在一行中从 typing
模块导入多个特定的类,例如:
1 | from typing import Any, Dict, Optional |
以此方式导入的类将被加入到本地的命名空间,因此所有 typing
模块中的类都应被视为关键字,不要在代码中定义并覆盖它们.若这些类和现行代码中的变量或者方法发生命名冲突,可以考虑使用 import x as y
的导入形式:
1 | from typing import Any as AnyType |
条件导入
在一些特殊情况下,比如当在运行时需要避免类型检查所需的一些导入时,可能会用到条件导入.但这类方法并不推荐,首选方法应是重构代码使类型检查所需的模块可以在顶层导入.
仅用于类型注解的导入可以放在 if TYPE_CHECKING:
语句块内.
- 通过条件导入引入的类的注解须是字符串string,这样才能和python3.6之前的代码兼容.因为python3.6之前,类型注解是会进行求值的.
- 条件导入引入的包应仅仅用于类型注解,别名也是如此.否则,将引起运行错误,条件导入的包在运行时是不会被实际导入的.
- 条件导入的语句块应放在所有常规导入的语句块之后.
- 在条件导入的语句块的导入语句之间不应有空行.
- 和常规导入一样,请对该导入语句进行排序.
1 | import typing |
循环依赖
由类型注释引起的循环依赖可能会导致代码异味,应对其进行重构.虽然从技术上我们可以兼容循环依赖,但是 构建系统 是不会容忍这样做的,因为每个模块都需要依赖一个其他模块.
将引起循环依赖的导入模块使用 Any
导入.使用 alias
来起一个有意义的别名,推荐使用真正模块的类型名的字符串作为别名(Any的任何属性依然是Any,使用字符串只是帮助我们理解代码).别名的定义应该和最后的导入语句之间空一行.
1 | from typing import Any |
泛型
在注释时,尽量将泛型类型注释为类型参数.否则, 泛型参数将被视为是 Any
1 |
|
若实在要用 Any 作为泛型类型,请显式的使用它.但在多数情况下, TypeVar
通常可能是更好的选择.
1 | def get_names(employee_ids: List[Any]) -> Dict[Any, Text]: |
总结
请务必保持代码的一致性
如果你正在编辑代码, 花几分钟看一下周边代码, 然后决定风格. 如果它们在所有的算术操作符两边都使用空格, 那么你也应该这样做. 如果它们的注释都用标记包围起来, 那么你的注释也要这样.
制定风格指南的目的在于让代码有规可循, 这样人们就可以专注于”你在说什么”, 而不是”你在怎么说”. 我们在这里给出的是全局的规范, 但是本地的规范同样重要. 如果你加到一个文件里的代码和原有代码大相径庭, 它会让读者不知所措. 避免这种情况.