目录
PEP(Python Enhancement Proposals)简介
文档字符串( Documentation Strings )
- 有助于增强代码的一致性和可读性。代码被阅读的次数远大于它被编写的次数,良好的遵循编码规范可以保证代码在一个项目中,甚至多个项目之间保持一致性和可读性;
- 有助于提高代码的可维护性和代码质量。易于理解的变量名称,清晰的代码布局,风格一致的注释等,都有助于降低开发和维护的难度,减少由于不遵循规范而产生的晦涩难懂的代码,并降低 bug 出现的可能性;
- 有助于提高软件团队的开发和协作效率。团队成员之间可以快速的阅读和理解对方所编写的代码,将更多的时间和精力投入更加核心的业务中去;
- 有助于提高项目和产品的交付质量。团队的效率和代码的质量,往往影响着项目的最终结果,也是产品质量与市场竞争力的决定性因素之一。
PEP 的英文全称是 Python Enhancement Proposals,即 Python 增强提案。它主要用于提出 Python 新特性、收集社区对于某些问题的讨论、记录 Python 核心的设计决策等。 在形式上来说,PEP 是一种技术文档;在内容上来说,一般会包括提供给 Python 社区的信息、新特性的功能及原理描述等内容
pep 分类:
PEP 8 的英文全称为 Style Guide for Python Code ,即 Python 编码风格指南(或称规范),这份指南涵盖三个大的方面,代码布局、注释文档以及命名规范
代码的整体布局主要囊括了代码在整体结构上应该注意的事项,比如用来区分代码块层次的缩进应使用 4 个空格、代码中函数与类之间应使用两个空行进行分隔等
Python 在语法上使用缩进来确定代码块的开始和结束,对于每一级缩进,应为 4 个空格,并且不要混用空格与制表符,缩进本身是一种语法上的限制,是强制性的
当需要换行时,换行后的内容应垂直对齐被包裹的元素。通常,换行时的缩进有两种建议的方式,一种是使用括号内的隐式换行形式
def function_name(var_one, var_two,
var_three, var_four):
pass
需要注意的是,使用悬挂缩进时第一行不应放置元素,并且当元素与下面内容的行相同缩进层次时,可以增加缩进来区分
# 第一行不应放置元素
function_name(
1, 2,
3, 4)
# 元素与下面的行相同缩进时,应增加缩进进行区分
def function_name(
var_one, var_two,
var_three, var_four):
print(var_one, var_two, var_three, var_four)
当 if 等控制语句的条件部分需要换行时,可以使用括号将条件部分包裹起来,在括号内进行换行。
if (
condition_one and
condition_two
):
pass
当多行结构在结束时,其右括号可以另起一行与前一行元素的第一个字符对齐或与第一行元素第一个字符对齐。
one_list = [
1, 2, 3,
4, 5, 6
]
two_list = [
1, 2, 3,
4, 5, 6
]
所有行的最大长度应限制在 79 个字符以内,文档字符串和注释的最大长度应限制在 72 个字符以内。当一个代码行的长度超过限制时应进行换行,换行应优先使用括号内隐式换行,其次才是使用反斜杠
# 应优先使用括号内隐式换行
if (
condition_one and
condition_two
):
pass
# 不提倡下面的方式
if condition_one and \
condition_two:
pass
##下面 with 中有多个语句时,可使用反斜杠。
with open(path_one) as file_one, \
open(path_one) as file_two:
pass
二元运算符之前换行
result = (var_one
- var_two
+ (var_three * var_four)
- var_five)
函数与类之间应用两个空行隔开,类中的方法之间应用一个空行隔开,函数或方法中的不同功能的代码块可使用空行隔开
导入的位置应在该文件头部,位于模块注释和文档字符串之后,全局变量和常量之前
对不同包或模块的导入,在代码结构上应该位于不同的行。针对同一个包或模块中进行的多个导入,可以位于同一行
如果你的代码中有多个导入,那么组织导入的整体顺序应首先为标准库导入,接着是相关第三方库导入,最后是本地代码导入。这三个部分之间可以用一个空行隔开
对于导入的形式,选择 import <module>.<name>
、from <module> import <name>
、from .<module> import <name>
均可(前两种形式称为绝对导入,最后一种称为相对导入或显式相对导入)。
推荐优先使用绝对路径进行导入(即绝对导入,absolute imports ),绝对导入的方式更具可读性,并且在导入系统配置不正确时可以表现得更好,比如给出更加清晰可读的错误信息。
在处理布局较为复杂的导入时,绝对路径的导入写法可能会造成导入语句过长,这时可考虑使用显式相对导入( explicit relative imports ),应避免使用隐式相对导入( implicit relative imports )
尽量避免使用 from <module> import *
的通配符导入写法,通配符导入的写法会使当前的命名空间变得混乱,可能会造成不必要的混淆。
PyCharm 组织导入的快捷键 Ctrl + Shift + O
调整导入的顺序
编码声明指的是类似 # -*- coding: <encoding name> -*-
形式的声明
dunder 的含义,它是一个合成词,即双下划线 double-underscore 。dunder 方法便指的是 Python 中的特殊方法,也可称为魔术方法,这些方法以双下划线开始和结尾,在运算符重载、数据结构定义中起着重要的作用,常规方法来说,不应使用这种双下划线开始和结尾的形式
"""
module docstrings
"""
from __future__ import unicode_literals
__all__ = ['testa', 'testb']
__version__ = '1.0'
__author__ = 'zhangsan'
import json
对单引号和双引号没有特殊的使用要求,它们的作用是相同的
"..."
u"你好世界"
'...'
r"..."
"""......"""
# 当一个字符串需要包含单或双引号时,应使用与最外层不同的引号,
# 来避免进行转义(转义会降低代码的可读性)
my_name = 'my name is "python"'
# 不提倡下面的方式
my_name = 'my name is \'python\''
[=,-,+=,==,>,in,is not, and]
:,
之后要有空格# 正确的写法
i = i + 1
submitted += 1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)
# 不推荐的写法
i=i+1
submitted +=1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
# 正确的写法
def complex(real, imag):
pass
# 不推荐的写法
def complex(real,imag):
pass
# 正确的写法
def complex(real, imag=0.0):
pass
# 不推荐的写法
def complex(real, imag = 0.0):
pass
# 正确的写法
spam(ham[1], {eggs: 2})
# 不推荐的写法
spam( ham[1], { eggs : 2 } )
# 正确的写法
dict['key'] = list[index]
# 不推荐的写法
dict ['key'] = list [index]
# 正确的写法
x = 1
y = 2
long_variable = 3
# 不推荐的写法
x = 1
y = 2
long_variable = 3
在多数场景中不应使用无关的空格
function_name('test', [1, 2, 3])
# 不提倡下面的方式
function_name( 'test' , [1, 2, 3] )
在切片操作中,冒号两边的空格数量应相同;在扩展切片(Extended Slices ,是指在切片中使用可选的第三个参数 step
,可称之为步长,默认为 1 )中,两个冒号应使用相同的空格数量;当切片中的某个参数被省略时,对应位置不应使用空格
my_list[1:6], my_list[var_one + var_two : var_three + var_four]
my_list[1:6:2], my_list[: func_one(var_one) : func_two(var_two)]
# 不提倡下面的方式
my_list[1 :6], my_list[var_one + var_two:var_three + var_four]
my_list[1:6 :2], my_list[ : func_one(var_one) : func_two(var_two)]
在函数名称与参数列表之间不应使用无关空格;在索引或切片中,括号和字典 / 列表等对象之间不应使用无关空格
function_name(var_name) my_list[1] my_dict['key'] # 不提倡下面的方式 function_name (var_name) my_list [1] my_dict ['key']
在赋值语句中,不应为了对齐赋值语句而使用无关空格。在表达式或语句的尾部不应使用无关空格,尾部的空格会在某些场景下造成问题,比如在使用反斜杠进行换行时,在反斜杠后添加空格,会引发SyntaxError: unexpected character after line continuation character
的异常
var_one = 1
long_var_two = 2
# 不提倡下面的方式
var_one = 1
long_var_two = 2
在函数中,对于形参列表中的默认参数以及函数调用时的关键字参数,其 =
两侧不应使用无关空格。
def function_one(var_one=True):
return var_one
function_one(var_one=False)
在运算符两侧合理使用空格
在赋值、增强型赋值、比较、布尔等二元运算符两侧应使用空格
var_one = 1
var_two = var_one + 1
var_one += 1
在同时使用不同优先级的运算符时,可在最低优先级的运算符两侧使用空格。对于一些比较复杂的场景下,需要我们来自己定夺空格的使用
var_two = var_one*3 + 1
var_five = var_one*var_two + var_three*var_four
# 注意下面的情况,PEP 8 把这种情况交由我们自己定夺,但仍给出了类似下面的示例供我们参考
var_five = (var_one+var_two) * (var_three+var_four)
# 不提倡下面的方式
var_five = (var_one + var_two) * (var_three + var_four)
在函数注解中合理使用空格
在函数注解中,冒号前不应使用无关空格,->
两侧应使用空格
def function_one(var_one: bool) -> int:
if var_one:
ret = 1
else:
ret = -1
return ret
当函数注解和默认参数同时存在于同一参数时,在 =
两侧应使用空格
def function_one(var_one: bool = True, var_two=False) -> int:
pass
函数注解( Function annotations )是 Python 3 提供的一种可选的语法,可以对函数的参数和返回值进行注解。在该特性在 3.0 中被加入后,官方并未给出其明确的语义,随着社区中相关经验的积累以及官方相关新特性的释放,函数注解目前主要用于类型提示,常结合 typing
模块使用
当你想要获得包含一个元素的元组时,应在表达式尾部添加逗号,并最好将其包裹在圆括号内
var_one = (1,)
# 最好不使用下面的方式
var_one = 1,
对于某些包含参数并需要不断扩展的序列,在添加每个参数时,在其尾部也可添加逗号,这样可以规避一些难以发现的问题(请不要小看下方代码所示的问题,当它隐藏在某一段代码中时,你就会感到十分头疼)
# 当在新行中添加参数时,在尾部添加逗号有利于后续扩展,比如可避免漏写上一行结尾处逗号的问题
BACKENDS = (
'BackendOne',
'BackendTwo',
)
# 在扩展时若漏写第二行结尾处的逗号,IDE 中也不会有任何提醒,但会导致无法正常使用该数据
BACKENDS = (
'BackendOne',
'BackendTwo'
'BackThree'
)
复合语句是指包含其他语句的语句,比如函数和类的定义、if
、while
、for
、try
、with
等语句,不应将整个复合语句或复合语句中的一部分放置在一行
if var_one == 1:
pass
# 不提倡下面的方式
if var_one == 1: pass
注释对于代码的阅读、扩展以及维护都非常重要。在 Python 中常用的注释方式有三种,分别为块注释( Block Comments )、行内注释( Inline Comments )、文档字符串( Documentation Strings )。首先我们需要了解的是在注释编写过程中的一些通用原则,这些原则不管针对哪种注释方式,都是值得参考的
应注重注释的可读性和完整性,这样有助于代码后续的扩展和维护;
应注重注释的实时性,即代码在修改或扩展过程中,及时更新对应的注释;
应优先使用英文编写注释
注释应为完整的句子;
当注释的开头不为以小写字母开始的标识符时,应大写首字母;
在包含多个句子的注释中,在非结尾句的结尾处可以使用两个空格等。
“#”号后空一格,段落间用空行分开(同样需要“#”号)
# 块注释
# 块注释
#
# 块注释
# 块注释
块注释在代码内部比较常见,其通常由一至多个段落构成,段落之间应使用开头为 #
的空行隔开,每个段落由完整的句子构成,在每行以 #
和一个空格开始。另外,块注释和被注释代码之间应具有同级别的缩进,这样有助于区分注释和代码之间关联关系。
通常我们提到的这些细节和准则在 Python 标准库的编写中往往做的更好,我们以 Python 标准库 collections
中 Counter
对象的 fromkeys
方法的注释来进行示例说明,这段注释主要说明了在 Counter
类中没有定义 fromkeys
方法的原因
@classmethod
def fromkeys(cls, iterable, v=None):
# There is no equivalent method for counters because the semantics
# would be ambiguous in cases such as Counter.fromkeys('aaabbc', v=2).
# Initializing counters to zero values isn't necessary because zero
# is already the default value for counter lookups. Initializing
# to one is easily accomplished with Counter(set(iterable)). For
# more exotic cases, create a dictionary first using a dictionary
# comprehension or dict.fromkeys().
raise NotImplementedError(
'Counter.fromkeys() is undefined. Use Counter(iterable) instead.')
行内注释是一种形式相对简单的注释,它和表达式或语句位于同一行,之间应通常使用两个空格隔开,注释部分应以 #
和一个空格开始。需要注意的一点是,在编写行内注释时,其描述要尽量明确,对于一些代码所表示的含义或逻辑显而易见时,行内注释没有必要再对其进行复述
# 不提倡下面的方式, 是因为显而易见的
my_list = [] # Make a list
#至少使用两个空格和语句分开,注意不要使用无意义的注释
# 正确的写法
x = x + 1 # 边框加粗一个像素
# 不推荐的写法(无意义的注释)
x = x + 1 # x加1
在 Python 中,最重要的注释形式便是文档字符串( Documentation Strings ),我们也常称之为 “docstrings”,一方面用来对模块、类、函数、方法等进行说明;另一方面可以配合一些辅助工具来自动化生成代码文档(关于这部分,我们会在后面的小节中专门说明)。
我们现在或许还不能完全意识到文档字符串的重要性,但如果你了解开源软件的话,你一定会知道一个开源软件文档质量的好坏有时会决定它是否会流行起来。
对于文档字符串的形式来说,它通常是模块、类、方法、函数定义中的首个语句,使用类似 """docstrings"""
这样的形式。若在文档字符串中存在 \
,可使用 r"""docstrings"""
形式;若在文档字符串中使用 unicode,可使用 u"""docstrings"""
。对于所有模块、被另外的模块导入的类或函数、类中的公共方法(包括构造函数),均应编写文档字符串。对于包来说,也可在其 __init__.py
中编写文档字符串。
单行文档字符串结构较为简单,在单行文档字符串后不应有空行,避免对显而易见的含义进行复述。
>>> def function_name():
... """docstrings"""
... pass
...
对于多行文档字符串来说,它的结构相对复杂,主要由引号、摘要行、空行、文档描述组成。整个文档字符串的缩进和代码首部缩进级别应相同,摘要行和首部引号可在同一行,也可在首部引号下一行,尾部引号单独成行。
对于普通类,其文档字符串可包含其实例属性、公共方法等的简要说明,在其文档字符串后可增加一个空行;对于继承于父类的子类,除了类的文档字符串中的内容外,可说明子类和父类的差异,比如对于重写父类的方法可使用 override 结合其他内容进行表示,对于调用父类方法并进行内容扩展的类方法,可使用 extend 结合其他内容进行表示;对于函数、方法,其文档字符串可包含参数、返回值说明、有可能抛出的异常以及功能等的简要说明;对于模块的文档字符串,可包含模块中可导入的类、函数等的简要说明;
对于包,其文档字符串可包含可导入的子包、模块等的简要说明(在 PEP 257 详细的描述了文档字符串的更多细节,感兴趣的同学可以查阅)。在这里我们分别以以 Python 标准库 os
中的一段文档字符串和第三方库 Tornado
中的一段文档字符串来进行示例说明
# Tornado
def import_object(name: str) -> Any:
"""Imports an object by name.
``import_object('x')`` is equivalent to ``import x``.
``import_object('x.y.z')`` is equivalent to ``from x.y import z``.
>>> import tornado.escape
>>> import_object('tornado.escape') is tornado.escape
True
>>> import_object('tornado.escape.utf8') is tornado.escape.utf8
True
>>> import_object('tornado') is tornado
True
>>> import_object('tornado.missing_module')
Traceback (most recent call last):
...
ImportError: No module named missing_module
"""
if name.count(".") == 0:
return __import__(name)
parts = name.split(".")
obj = __import__(".".join(parts[:-1]), fromlist=[parts[-1]])
try:
return getattr(obj, parts[-1])
except AttributeError:
raise ImportError("No module named %s" % parts[-1])
在 PyCharm 中我们很方便的编写函数或方法的文档字符串,比如我们编写了这种形式的函数后:
def function_name(var_one, var_two: bool = False):
""""""
pass
只需要在引号中间回车,PyCharm 会帮助我们生成一个标准的结构
def function_name(var_one, var_two: bool = False):
"""
:param var_one:
:param var_two:
:return:
"""
pass
文档字符串可通过对象的 __doc__
属性查看,同时也可借助 Python 的内置函数 help()
来查看。同时,编写良好的文档字符串还可以结合自动化工具帮助我们生成代码文档,比如 Python 自带的 pydoc
命令,上面提到的第三方工具 sphinx
等
>>> def function_name():
... """docstrings"""
... pass
...
>>> function_name.__doc__
'docstrings'
>>> help(function_name)
Help on function function_name in module __main__:
function_name()
docstrings
m_bCanRun
,其中 m_
表示其为成员变量,b
表示其类型为布尔值,CanRun
表示其代表是否可以检查的含义。canRun
,由于首字母小写,这种形式也被称为小驼峰命名法。CanRun
,也被称为大驼峰命名法。_
连接,比如 can_run
。通用准则:
os
模块中 makedirs
、removedirs
等函数的命名,我们可以清楚的知道这些函数的使用场景,而不用关心其内部实现;tools
、utils
、common
等名称,以及避免使用以 object
、manager
/ management
、handler
等作为后缀的名称,这些观点中将其称之为反模式,认为这些名称没有起到实际的意义,并且类似 utils
的命名反而最终会成为劣质代码的聚集地,并且将这种名称认为是缺乏设计的名称,我们在这里将这些观点当作一种扩展阅读即可;对于变量(和常量):
const
的概念,常量仅仅是一种约定(常量通常放置在代码顶部、或单独的模块、或特定的配置文件中)。对于一组常量,不应使用同一个前缀(这也适用于在同一个类中的方法或者属性),因为这样会和模块名称造成冗余,若多组常量,则针对每一组常量可以使用同一前缀。key_value
的形式,其中 key
、value
为键和值的实际含义。has
、is
作为前缀。对于类(和异常):
int
、list
),类和属性常采用名词,方法多采用动词或者包含动词。Base
作为前缀,抽象类常采用 Abstract
作为前缀。Error
后缀(并不是所有异常都代表代码运行错误,比如 KeyboardInterrupt
、SystemExit
)。对于函数、方法:
threading
模块中,因为这些代码的出现往往早于 PEP 8 规范的诞生,同时为了向后兼容而保留,但通常这些模块都会提供相同功能的以小写加下划线命名的方法,我们应该尽可能使用这些新的方法)。self
,类方法的首个参数应为 cls
( class
作为关键字不能被使用,常被 cls
或 klass
替换);对于模块和包:
__init__
模块以外),当使用 C/C++ 编写扩展模块时,通常需要添加下划线前缀;特殊格式:
_name
,常称为 ”伪私有属性“(也可用于私有的方法、类等),这种命名方式在 Python 中是一种表示私有属性的约定(同时注意 from ... import *
时不会导入这种形式的对象,并且这种伪私有变量通常会通过特定的方法来获取或者赋值),私有属性通常没有直接对外的功能,常用于记录内部状态或用于提供一些公共功能的方法内使用;name_
,常用于避免和 Python 的关键字发生冲突;__name
,常用于基类避免和子类中的命名冲突,Python 会通过转换规则将其转换为类似 _Class__name
的形式,这种方式通常称为命名修饰 name mangling 或 name decoration(这种方式通常用于多继承,应避免将这种形式用在私有属性的命名上);>>> class MyClass:
... __name = 1
...
>>> MyClass.__name # 无法直接访问
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: type object 'MyClass' has no attribute '__name'
>>> MyClass._MyClass__name
1
__name__
,这是我们之前提到过的 Python 内部常用的 dunder 名称,不应自定义这类命名。模块尽量使用小写命名,首字母保持小写,尽量不要用下划线(除非多个单词,且数量不多的情况)
# 正确的模块名
import decoder
import html_parser
# 不推荐的模块名
import Decoder
类名使用驼峰(CamelCase)命名风格,首字母大写,私有类可用一个下划线开头
class Farm():
pass
class AnimalFarm(Farm):
pass
class _PrivateFarm(Farm):
pass
将相关的类和顶级函数放在同一个模块里. 不像Java, 没必要限制一个类一个模块.
函数名一律小写,如有多个单词,用下划线隔开
def run():
pass
def run_with_env():
pass
私有函数在函数前加一个下划线_
class Person():
def _private_func():
pass
变量名尽量小写, 如有多个单词,用下划线隔开
if __name__ == '__main__':
count = 0
school_name = ''
常量采用全大写,如有多个单词,使用下划线隔开
MAX_CLIENT = 100
MAX_CONNECTION = 1000
CONNECTION_TIMEOUT = 600
常量使用以下划线分隔的大写命名
MAX_OVERFLOW = 100
Class FooBar:
def foo_bar(self, print_):
print(print_)
PyCharm 中已经内置了 pycodestyle.py
进行检查,在 PyCharm 右下角或在 Settings 中搜索 Inspections :
你也可以在选择忽略某些检查提示,在 Ignore errors 中添加或者在 IDE 中检查提示浮窗中选择 More actions…,点击 Ignore errors like this。
pylint 是一款代码检查与分析工具(类似的可以进行静态检查的工具还有 flake8 等),它不仅可以对代码中违反 PEP 8 的部分进行检查,还可以对代码中常见的错误进行静态分析。同时,pylint 可以很方便的集成进 PyCharm 中。
black 是一款相对较新的代码格式化工具(类似的格式化工具还有 autopep8、yapf 等),配置项少是它最大的特点,这通常对于刚开始学习 Python 编码规范的读者来说是一件好事,不必关注太多细节便可以写出相对规范的 Python 代码。