PEP 8风格指南

PEP是Python Enhancement Proposal的缩写,通常翻译为“Python增强提案”。每个PEP都是一份为Python社区提供的指导Python往更好的方向发展的技术文档,其中的第8号增强提案(PEP 8)是针对Python语言编订的代码风格指南。尽管我们可以在保证语法没有问题的前提下随意书写Python代码,但是在实际开发中,采用一致的风格书写出可读性强的代码是每个专业的程序员应该做到的事情,也是每个公司的编程规范中会提出的要求,这些在多人协作开发一个项目(团队开发)的时候显得尤为重要。我们可以从Python官方网站的PEP 8链接中找到该文档,下面我们对该文档的关键部分做一个简单的总结。

分号

不要在行尾加分号, 也不要用分号将两条命令放在同一行。

行长度

每行不超过80个字符。

例外:

  • 长的导入模块语句
  • 注释里的URL
  • 不要使用反斜杠连接行

Python会将圆括号,中括号花括号中的行隐式的连接起来 , 你可以利用这个特点. 如果需要, 你可以在表达式外围增加一对额外的圆括号。

1
2
3
4
5
Yes: foo_bar(self, width, height, color='black', design=None, x='foo',
emphasis=None, highlight=0)

if (width == 0 and height == 0 and
color == 'red' and emphasis == 'strong'):

如果一个文本字符串在一行放不下, 可以使用圆括号来实现隐式行连接:

1
2
x = ('This will build a very long long '
'long long long long long long string')

在注释中,如果必要,将长的URL放在一行上。

1
2
Yes:  # See details at
# http://www.example.com/us/developer/documentation/api/content/v2.0/csv_file_name_extension_full_specification.html
1
2
3
No:  # See details at
# http://www.example.com/us/developer/documentation/api/content/\
# v2.0/csv_file_name_extension_full_specification.html

括号

宁缺毋滥的使用括号。
除非是用于实现行连接, 否则不要在返回语句或条件语句中使用括号。 不过在元组两边使用括号是可以的。

1
2
3
4
5
6
7
8
9
10
Yes: if foo:
bar()
while x:
x = bar()
if x and y:
bar()
if not x:
bar()
return foo
for (x, y) in dict.items(): ...
1
2
3
4
5
No:  if (x):
bar()
if not(x):
bar()
return (foo)

空格的使用

  • 使用空格来表示缩进而不要用制表符(Tab)。这一点对习惯了其他编程语言的人来说简直觉得不可理喻,因为绝大多数的程序员都会用Tab来表示缩进,但是要知道Python并没有像C/C++或Java那样的用花括号来构造一个代码块的语法,在Python中分支和循环结构都使用缩进来表示哪些代码属于同一个级别,鉴于此Python代码对缩进以及缩进宽度的依赖比其他很多语言都强得多。在不同的编辑器中,Tab的宽度可能是2、4或8个字符,甚至是其他更离谱的值,用Tab来表示缩进对Python代码来说可能是一场灾难。
  • 和语法相关的每一层缩进都用4个空格来表示。
  • 除了首行之外的其余各行都应该在正常的缩进宽度上再加上4个空格。
  • 函数和类的定义,代码前后都要用两个空行进行分隔。
  • 在同一个类中,各个方法之间应该用一个空行进行分隔。
  • 二元运算符的左右两侧应该保留一个空格,而且只要一个空格就好。

标识符命名

PEP 8倡导用不同的命名风格来命名Python中不同的标识符,以便在阅读代码时能够通过标识符的名称来确定该标识符在Python中扮演了怎样的角色(在这一点上,Python自己的内置模块以及某些第三方模块都做得并不是很好)。

  • 变量、函数和属性应该使用小写字母来拼写,如果有多个单词就使用下划线进行连接。
  • 类中受保护的实例属性,应该以一个下划线开头。
  • 类中私有的实例属性,应该以两个下划线开头。
  • 类和异常的命名,应该每个单词首字母大写。
  • 模块级别的常量,应该采用全大写字母,如果有多个单词就用下划线进行连接。
  • 类的实例方法,应该把第一个参数命名为self以表示对象自身。
  • 类的类方法,应该把第一个参数命名为cls以表示该类自身。

表达式和语句

在Python之禅(可以使用import this查看)中有这么一句名言:“**There should be one– and preferably only one –obvious way to do it.**”,翻译成中文是“做一件事应该有而且最好只有一种确切的做法”,这句话传达的思想在PEP 8中也是无处不在的。

  • 采用内联形式的否定词,而不要把否定词放在整个表达式的前面。例如if a is not b就比if not a is b更容易让人理解。
  • 不要用检查长度的方式来判断字符串、列表等是否为None或者没有元素,应该用if not x这样的写法来检查它。
  • 就算if分支、for循环、except异常捕获等中只有一行代码,也不要将代码和if、for、except等写在一起,分开写才会让代码更清晰。
  • import语句总是放在文件开头的地方。
  • 引入模块的时候,from math import sqrt比import math更好。
  • 如果有多个import语句,应该将其分为三部分,从上到下分别是Python标准模块、第三方模块和自定义模块,每个部分内部应该按照模块名称的字母表顺序来排列。

注释

确保对模块, 函数, 方法和行内注释使用正确的风格。

块注释和行注释

最需要写注释的是代码中那些技巧性的部分。如果你在下次代码审查的时候必须解释一下,那么你应该现在就给它写注释。对于复杂的操作, 应该在其操作开始前写上若干行注释。对于不是一目了然的代码, 应在其行尾添加注释。

1
2
3
4
5
6
# 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.

if i & (i-1) == 0: # True if i is 0 or a power of 2.

为了提高可读性,注释应该至少离开代码2个空格。
另一方面, 绝不要描述代码。 假设阅读代码的人比你更懂Python, 他只是不知道你的代码要做什么。

文档字符串

Python有一种独一无二的的注释方式: 使用文档字符串。

文档字符串是包, 模块,类或函数里的第一个语句。
这些字符串可以通过对象的__doc__成员被自动提取, 并且被pydoc所用。(你可以在你的模块上运行pydoc试一把, 看看它长什么样)。
我们对文档字符串的惯例是使用三重双引号“””( PEP-257 )。

一个文档字符串应该这样组织:

  • 首先是一行以句号,问号或惊叹号结尾的概述(或者该文档字符串单纯只有一行)。
  • 接着是一个空行。
  • 接着是文档字符串剩下的部分,它应该与文档字符串的第一行的第一个引号对齐。
函数文档字符串

一个函数必须要有文档字符串,除非它满足以下条件:

  • 外部不可见
  • 非常短小
  • 简单明了

文档字符串应该包含函数做什么以及输入和输出的详细描述。通常不应该描述“怎么做”,除非是一些复杂的算法。

文档字符串应该提供足够的信息,当别人编写代码调用该函数的时候,它不需要看一行代码,只要看文档字符串就够了。对于复杂的代码,在代码旁边加注释会比文档字符串更有意义。
函数的文档字符串格式,将函数按照参数、返回值、抛出异常等信息分小节进行描述,每小节应该以一个标题行开始,标题行以冒号结尾,除标题行外, 节的其他内容应被缩进2个空格。

1
2
3
4
5
6
Args:
列出每个参数的名字, 并在名字后使用一个冒号和一个空格, 分隔对该参数的描述.如果描述太长超过了单行80字符,使用2或者4个空格的悬挂缩进(与文件其他部分保持一致). 描述应该包括所需的类型和含义. 如果一个函数接受*foo(可变长度参数列表)或者**bar (任意关键字参数), 应该详细列出*foo和**bar.
Returns: (或者 Yields: 用于生成器)
描述返回值的类型和语义. 如果函数返回None, 这一部分可以省略.
Raises:
列出与接口有关的所有异常.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def fetch_bigtable_rows(big_table, keys, other_silly_variable=None):
"""Fetches rows from a Bigtable.

Retrieves rows pertaining to the given keys from the Table instance
represented by big_table. Silly things may happen if
other_silly_variable is not None.

Args:
big_table: An open Bigtable Table instance.
keys: A sequence of strings representing the key of each table row
to fetch.
other_silly_variable: Another optional variable, that has a much
longer name than the other args, and which does nothing.

Returns:
A dict mapping keys to the corresponding table row data
fetched. Each row is represented as a tuple of strings. For
example:

{'Serak': ('Rigel VII', 'Preparer'),
'Zim': ('Irk', 'Invader'),
'Lrrr': ('Omicron Persei 8', 'Emperor')}

If a key from the keys argument is missing from the dictionary,
then that row was not found in the table.

Raises:
IOError: An error occurred accessing the bigtable.Table object.
"""
pass
类文档字符串

类应该在其定义下有一个用于描述该类的文档字符串。 如果你的类有公共属性(Attributes), 那么文档中应该有一个属性(Attributes)段。 并且应该遵守和函数参数相同的格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.
"""

def __init__(self, likes_spam=False):
"""Inits SampleClass with blah."""
self.likes_spam = likes_spam
self.eggs = 0

def public_method(self):
"""Performs operation blah."""

惯例

“惯例”这个词指的是“习惯的做法,常规的办法,一贯的做法”,与这个词对应的英文单词叫“idiom”。由于Python跟其他很多编程语言在语法和使用上还是有比较显著的差别,因此作为一个Python开发者如果不能掌握这些惯例,就无法写出“Pythonic”的代码。下面我们总结了一些在Python开发中的惯用的代码。

让代码既可以被导入又可以被执行。

1
if __name__ == '__main__':

用下面的方式判断逻辑“真”或“假”。

1
2
if x:
if not x:

好的代码:

1
2
3
4
5
name = 'Safe'
pets = ['Dog', 'Cat', 'Hamster']
owners = {'Safe': 'Cat', 'George': 'Dog'}
if name and pets and owners:
print('We have pets!')

不好的代码:

1
2
if name != '' and len(pets) > 0 and owners != {}:
print('We have pets!')

善于使用in运算符。

1
2
if x in items: # 包含
for x in items: # 迭代

好的代码:

1
2
3
name = 'Safe Hammad'
if 'H' in name:
print('This name has an H in it!')

不好的代码:

1
2
3
name = 'Safe Hammad'
if name.find('H') != -1:
print('This name has an H in it!')

不使用临时变量交换两个值。

1
a, b = b, a

用序列构建字符串。

好的代码

1
2
3
chars = ['S', 'a', 'f', 'e']
name = ''.join(chars)
print(name) # Safe

不好的代码

1
2
3
4
5
chars = ['S', 'a', 'f', 'e']
name = ''
for char in chars:
name += char
print(name) # Safe

EAFP 优于 LBYL

“It’s Easier to Ask for Forgiveness than Permission.”
“Look Before You Leap”

好的代码

1
2
3
4
5
d = {'x': '5'}
try:
value = int(d['x'])
except (KeyError, TypeError, ValueError):
value = None

不好的代码

1
2
3
4
5
6
7
d = {'x': '5'}
if 'x' in d and \
isinstance(d['x'], str) and \
d['x'].isdigit():
value = int(d['x'])
else:
value = None

使用enumerate进行迭代。

好的代码

1
2
3
fruits = ['orange', 'grape', 'pitaya', 'blueberry']
for index, fruit in enumerate(fruits):
print(index, ':', fruit)

不好的代码

1
2
3
4
5
fruits = ['orange', 'grape', 'pitaya', 'blueberry']
index = 0
for fruit in fruits:
print(index, ':', fruit)
index += 1

用生成式生成列表。

好的代码

1
2
3
data = [7, 20, 3, 15, 11]
result = [num * 3 for num in data if num > 10]
print(result) # [60, 45, 33]

不好的代码

1
2
3
4
5
6
data = [7, 20, 3, 15, 11]
result = []
for i in data:
if i > 10:
result.append(i * 3)
print(result) # [60, 45, 33]

用zip组合键和值来创建字典。

好的代码

1
2
3
4
5
6
keys = ['Safe', 'Bob', 'Thomas']
values = ['Hammad', 'Builder', 'Engine']
d = dict(zip(keys, values))
print(d) # {'Bob': 'Builder',
# 'Safe': 'Hammad',
# 'Thomas': 'Engine'}

不好的代码

1
2
3
4
5
6
7
8
keys = ['Safe', 'Bob', 'Thomas']
values = ['Hammad', 'Builder', 'Engine']
d = {}
for i, key in enumerate(keys):
d[keys] = values[i]
print(d) # {'Bob': 'Builder',
# 'Safe': 'Hammad',
# 'Thomas': 'Engine'}

参考

PEP8风格指南

谷歌开源项目风格规范

惯例