Python中的编程规范

统一的编程规范为何重要?

本文为景霄-Python核心技术与实战的学习笔记,如要查看完整内容请点击链接。

统一的编程规范可以提高开发效率,开发效率涉及三类对象,即:阅读者、编程者和机器。三者的优先级是:阅读者的体验>>编程者的体验>>机器的体验

阅读者的体验 高于 编程者

在代码编写过程中,我们更多的时间是在阅读代码而不是写代码。代码质量的高低决定了别人是否可以很容易地读懂你的代码,从而节省开发时间。

如下述代码所示,变量名是否具有确定的含义直接影响着阅读体验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 错误示例
if (a <= 0):
return
elif (a > b):
return
else:
b -= a

# 正确示例
if (transfer_amount <= 0):
raise Exception('...')
elif (transfer_amount > balance):
raise Exception('...')
else:
balance -= transfer_amount

在Google Style 2.2条中规定,Python代码中的import对象只能是package或者module。

1
2
3
4
5
6
7
8
9
10
11
# 错误示例
from mypkg import Obj
from mypkg import my_func

my_func([1, 2, 3])

# 正确示例
import numpy as np
import mypkg

np.array([6, 7, 8])

通过导入包或者模块,在调用包中的内容时,我们可以很容易知道所调用的内容的上下文环境和来源。

编程者的体验 高于 机器

在编写代码时,我们经常会不自觉地追求极简的代码编写方式,例如盲目使用Python中的列表表达式:

1
2
# 错误示例
result = [(x, y) for x in range(10) for y in range(5) if x * y > 10]

实际上,类似的代码不仅具有较高的编写难度,同时也不利于代码的阅读这阅读。更简单的做法是:

1
2
3
4
5
6
# 正确示例
result = []
for x in range(10):
for y in range(5):
if x * y > 10:
result.append((x, y))

机器的体验也很重要

编写的代码最终需要在机器上运行,因而代码的效率也很重要。

is==的区别,如下述代码所示:

1
2
3
4
5
6
7
8
# 错误示例
x = 27
y = 27
print(x is y)

x = 721
y = 721
print(x is y)

is是比较内存地址的,因而第一个输出True,第二个输出False。原因在于,CPython将-5到256的整数存放在固定的内存空间中,指向这一范围内的同一个整数对象的变量会指向同一块内存地址。

但对于这个范围之外的整数,重新定义意味着重新分配内存,因而不同变量的地址自然不同。

因而,即使is比较对象的内存地址,也应该尽量避免使用is比较两个Python整数的地址。

因而比较整数对象应尽量使用:

1
2
3
4
5
6
7
8
# 正确示例
x = 27
y = 27
print(x == y)

x = 721
y = 721
print(x == y)

而在使用==时同样也应该注意:

1
2
3
# 错误示例
x = MyObject()
print(x == None)

上述代码的执行结果取决于MyObject()__eq__()方法的具体实现。正确的做法是,在和None进行比较的时候,要使用is

1
2
3
# 正确示例
x = MyObject()
print(x is None)

但是除此之外还要注意Python中的隐式布尔转换,比如:

1
2
3
4
5
# 错误示例
def pay(name, salary=None):
if not salary:
salary = 11
print(name, "is compensated", salary, "dollars")

当执行pay("Andrew", 0)时,0会被自动转换为布尔值,因而输出结果为:”Ahdrew is compensated 11 dollers”。因而,当明确想要比较对象是否是None时,一定要使用is None

1
2
3
4
5
# 正确示例
def pay(name, salary=None):
if salary is not None:
salary = 11
print(name, "is compensated", salary, "dollars")

同时,不规范的编程习惯会导致程序效率问题。

1
2
3
4
5
# 错误示例
adict = {i: i * 2 for i in xrange(10000000)}

for key in adict.keys():
print("{0} = {1}".format(key, adict[key]))

该代码的keys()方法会在遍历前生成一个临时的列表,导致消耗大量的内存并且运行缓慢。正确的做法是使用默认的iterator。默认的iterator不会重新分配内存。

1
2
# 正确示例
for key in adict:

PEP 8规范

PEP是Python Enhancement Proposal的缩写,即“Python增强规范”。其存在意义就是让Python更易阅读。

缩写规范

Python和C++ / Java的不同之处在于,后者使用大括号区分代码块,而Python完全使用不同行和不同的缩进来进行分块。

在进行缩进时,有多种选择,Tab、双空格、四空格、空格和Tab混合等。而PEP8的要求是:

使用四个空格的缩进,不要使用Tab,更不要使用Tab和空格的混用。

同时,每一行代码的长度要限制在79个字符以内。

这一规定有以下两个有点:

  • 当一行代码的字符数过多时,不利于程序员的阅读;
  • 当代码的嵌套层数过高时,比如超过三层之后,一行的内容很容易就会超过79个字符。因而,该规定也在另一方面约束程序员,不要使用过深的约束,而是要把代码继续分解为其他函数或者逻辑块,对代码的结构进行优化。

空行规范

PEP 8规定:

全局的类和函数的上方需要空两个空行,而类的函数之间需要空一个空行。同时,函数的内部也可以使用空行来区分不同的代码块,但最多只能使用一行。

同时,在代码的尾部,每个代码文件的最后一行为空行,并且只有一个空行。

空格规范

在函数的参数列表中,每一个参数的逗号后都要跟一个空格。目的在于使得每个参数都能够独立阅读。同理,冒号经常被用来初始化字典,因而冒号后也要跟一个空格。

在使用#进行单行注释时,要在#后、注释前添加一个空格。

另外,在操作符,例如+, -, *, /等的两端都要保留空格。但是,括号的两端不需要空格。

换行规范

以如下代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def solve1(this_is_the_first_parameter, this_is_the_second_parameter, this_is_the_third_parameter,
this_is_the_forth_parameter, this_is_the_fifth_parameter, this_is_the_sixth_parameter):
return (this_is_the_first_parameter + this_is_the_second_parameter + this_is_the_third_parameter +
this_is_the_forth_parameter + this_is_the_fifth_parameter + this_is_the_sixth_parameter)


def solve2(this_is_the_first_parameter, this_is_the_second_parameter, this_is_the_third_parameter,
this_is_the_forth_parameter, this_is_the_fifth_parameter, this_is_the_sixth_parameter):
return this_is_the_first_parameter + this_is_the_second_parameter + this_is_the_third_parameter + \
this_is_the_forth_parameter + this_is_the_fifth_parameter + this_is_the_sixth_parameter


(top_secret_func(param1=12345678, param2=12345678, param3=12345678, param4=12345678, param5=12345678).check()
.launch_nuclear_missile().wait())


top_secret_func(param1=12345678, param2=12345678, param3=12345678, param4=12345678, param5=12345678).check() \
.launch_nuclear_missile().wait()

在进行代码换行时,有两种经典的做法。

第一种:通过括号将过长的运算进行封装。此时虽然跨行,但是仍处于一个逻辑引用之内。对于solve1函数,由于参数过多,因而需要进行换行,但要注意的是第二行的参数要和函数的第一个参数对齐,这样做可以使得函数变得美观。

第二种:使用换行符实现

文档规范

在使用import语句时,要尽量将其放在文件的开头。同时,不要在同一行中一次导入过个模块,例如:不要使用import time, os

注释规范

对于大的逻辑块,可以在最开始相同的缩进处以#开始写注释。对于行注释,可以在一行后面跟两个空格,接着以#开头加入注释。但是,并不推荐使用行注释。

1
2
3
4
5
6
# This is an example to demonstrate how to comment.
# Please note this function must be used carefully.
def solve(x):
if x == 1: # This is only one exception.
return False
return True

文档描述

以TensorFlow的代码为例。

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
31
32
33
34
35
36
37
38
class SpatialDropout2D(Dropout):
"""Spatial 2D version of Dropout.
This version performs the same function as Dropout, however it drops
entire 2D feature maps instead of individual elements. If adjacent pixels
within feature maps are strongly correlated (as is normally the case in
early convolution layers) then regular dropout will not regularize the
activations and will otherwise just result in an effective learning rate
decrease. In this case, SpatialDropout2D will help promote independence
between feature maps and should be used instead.
Arguments:
rate: float between 0 and 1. Fraction of the input units to drop.
data_format: 'channels_first' or 'channels_last'.
In 'channels_first' mode, the channels dimension
(the depth) is at index 1,
in 'channels_last' mode is it at index 3.
It defaults to the `image_data_format` value found in your
Keras config file at `~/.keras/keras.json`.
If you never set it, then it will be "channels_last".
Input shape:
4D tensor with shape:
`(samples, channels, rows, cols)` if data_format='channels_first'
or 4D tensor with shape:
`(samples, rows, cols, channels)` if data_format='channels_last'.
Output shape:
Same as input
References:
- [Efficient Object Localization Using Convolutional
Networks](https://arxiv.org/abs/1411.4280)
"""
def __init__(self, rate, data_format=None, **kwargs):
super(SpatialDropout2D, self).__init__(rate, **kwargs)
if data_format is None:
data_format = K.image_data_format()
if data_format not in {'channels_last', 'channels_first'}:
raise ValueError('data_format must be in '
'{"channels_last", "channels_first"}')
self.data_format = data_format
self.input_spec = InputSpec(ndim=4)

类和函数的注释为的是让读者快速理解函数的作用,以及输入参数、输出的返回值和格式以及其他需要注意的地方。

而docstring的写法是以三个双引号开始,三个双引号结束。首先用一句话概述函数的所用,接着用一段话详细解释;接着是参数列表、参数格式和返回值格式。

命名规范

变量命名拒绝使用毫无意义的单字符。应该使用能改代表其意思的变量名,一般,变量使用小写,通过下划线串联。例如:data_format、input_spec等。唯一可以使用单字符的地方是迭代,如for i in range(n)。同时,如果是类的私有变量,要在变量前加两个下划线。

常量,最好的做法是全部大写。而对于函数名,同样使用小写的方式,通过下划线连接。如:launch_nuclear_missile()check_input_validation()

类名,应该首字母大写,然后合并起来,例如:class SpatialDropout2D()

代码分解技巧

编程的一个核心思想是不写重复代码,重复代码可以使用条件、循环、构造函数和类解决;另一个核心思想是减少迭代层数,尽可能让Python代码扁平化。

在业务逻辑比较复杂的地方,需要加入大量的判断和循环。但写好判断和循环需要注意很多细节问题。

1
2
3
4
5
6
if i_am_rich:
money = 100
send(money)
else:
money = 10
send(money)

上述代码中,send(mony)出现了两次,因而可以对其进行合并。

1
2
3
4
5
if i_am_rich:
money = 100
else:
money = 10
send(money)

再看下面一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def send(money):
if is_server_dead:
LOG('server dead')
return
else:
if is_server_timed_out:
LOG('server timed out')
return
else:
result = get_result_from_server()
if result == MONEY_IS_NOT_ENOUGH:
LOG('you do not have enough money')
return
else:
if result == TRANSACTION_SUCCEED:
LOG('OK')
return
else:
LOG('something wrong')
return

这段代码中使用了过多的缩进,改进如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def send(money):
if is_server_dead:
LOG('server dead')
return

if is_server_timed_out:
LOG('server timed out')
return

result = get_result_from_server()

if result == MONET_IS_NOT_ENOUGH:
LOG('you do not have enough money')
return

if result == TRANSACTION_SUCCEED:
LOG('OK')
return

LOG('something wrong')

同时,在编写函数时,一个函数的功能应尽可能地细,单个函数不应该承担过多的功能。对于一个复杂的函数,应该将其拆分为几个功能简单的函数,然后进行合并。

以二分搜索为例,给定一个非递减整数数组和一个target,要求找到数组中最小的一个数x,满足x*x>target,如果不存在则返回-1。

首先给出代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def solve(arr, target):
l, r = 0, len(arr) - 1
ret = -1
while l <= r:
m = (l + r) // 2
if arr[m] * arr[m] > target:
ret = m
r = m - 1
else:
l = m + 1
if ret == -1:
return -1
else:
return arr[ret]


print(solve([1, 2, 3, 4, 5, 6], 8))
print(solve([1, 2, 3, 4, 5, 6], 9))
print(solve([1, 2, 3, 4, 5, 6], 0))
print(solve([1, 2, 3, 4, 5, 6], 40))

可以继续优化如下:

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
def comp(x, target):
return x * x > target


def binary_search(arr, target):
l, r = 0, len(arr) - 1
ret = -1
while l <= r:
m = (l + r) // 2
if comp(arr[m], target):
ret = m
r = m - 1
else:
l = m + 1
return ret


def solve(arr, target):
id = binary_search(arr, target)

if id != -1:
return arr[id]
return -1


print(solve([1, 2, 3, 4, 5, 6], 8))
print(solve([1, 2, 3, 4, 5, 6], 9))
print(solve([1, 2, 3, 4, 5, 6], 0))
print(solve([1, 2, 3, 4, 5, 6], 40))

在上述改进代码中,不仅对功能进行了拆分,同时对二分搜索的主程序进行了提取,使其只负责二分搜索。

讲完了函数的拆分,下面来看看如何对类进行拆分。

1
2
3
4
5
6
7
8
class Person:
def __init__(self, name, sex, age, job_title, job_description, company_name):
self.name = name
self.sex = sex
self.age = age
self.job_title = job_title
self.job_description = description
self.company_name = company_name

在上述类中,job相关的属性有很多,并且表达的是同一个意义的实体,因而可以将这部分代码独立出来,作为独立的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person:
def __init__(self, name, sex, age, job_title, job_description, company_name):
self.name = name
self.sex = sex
self.age = age
self.job = Job(job_title, job_description, company_name)

class Job:
def __init__(self, job_title, job_description, company_name):

self.job_title = job_title
self.job_description = description
self.company_name = company_name

如何合理使用assert?

什么是assert?

Python的assert主要被用于测试一个条件是否满足,如果满足则不执行任何语句;如果不满足则抛出AssertionError,并返回具体的错误信息(可选)。

具体语法如下:

1
assert_stmt ::=  "assert" expression ["," expression]

例如:

1
assert 1 == 2

相当于如下代码:

1
2
if __debug__:
if not expression: raise AssertionError

还可以使用如下形式:

1
assert 1 == 2,  'assertion is wrong'

相当于:

1
2
if __debug__:
if not expression1: raise AssertionError(expression2)

这里的debug是一个常数。如果 Python 程序执行时附带了-O这个选项,比如Python test.py -O,那么程序中所有的 assert 语句都会失效,常数debug便为 False;反之debug则为 True。要注意的是,不能直接对debut赋值,这一操作是非法的。

同时,在使用assert时不能加入括号:

1
2
3
4
assert(1 == 2, 'This should fail')
# 输出
<ipython-input-8-2c057bd7fe24>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?
assert(1 == 2, 'This should fail')

assert 在程序中的作用,是对代码做一些 内部 的自检。使用 assert,就表示很确定。这个条件一定会发生或者一定不会发生。

assert的用法

假设在某个活动中需要进行打折促销,需要一个函数,输入为原来的价格和折扣,输出为最后的价格。那么可以编写代码如下:

1
2
3
4
def apply_discount(price, discount):
updated_price = price * (1 - discount)
assert 0 <= updated_price <= price, 'price should be greater or equal to 0 and less or equal to original price'
return updated_price

此时,进行如下测试:

1
2
3
4
5
apply_discount(100, 0.2)
80.0

apply_discount(100, 2)
AssertionError: price should be greater or equal to 0 and less or equal to original price

又以除法操作为例:

1
2
3
def calculate_average_price(total_sales, num_sales):
assert num_sales > 0, 'number of sales should be greater than 0'
return total_sales / num_sales

除此之外,还有一些很常见的做法:

1
2
3
4
5
6
7
8
9
def func(input):
assert isinstance(input, list), 'input must be type of list'
# 下面的操作都是基于前提:input必须是list
if len(input) == 1:
...
elif len(input) == 2:
...
else:
...

错误用法

假如,某个课程平台需要删除上线时间过长的一些专栏,于是有如下的专栏删除函数:

1
2
3
4
def delete_course(user, course_id):
assert user_is_admin(user), 'user must be admin'
assert course_exist(course_id), 'course id must exist'
delete(course_id)

在代码角度,这段代码没有问题。但是,在实际工程中是有问题的,一旦assert的检查被关闭,两个检查语句便会被跳过,导致程序运行异常。正确的做法是使用条件语句进行相应的检验:

1
2
3
4
5
6
def delete_course(user, course_id):
if not user_is_admin(user):
raise Exception('user must be admin')
if not course_exist(course_id):
raise Exception('coursde id must exist')
delete(course_id)

总的来说,assert 并不适用 run-time error 的检查。比如你试图打开一个文件,但文件不存在;再或者是你试图从网上下载一个东西,但中途断网了等等。

参考