Python 代码规范

规范参考


统一的编程规范的重要性

统一的编程规范能提高开发效率。而开发效率,关乎三类对象,也就是阅读者、编程者和机器。

他们的优先级是 阅读者的体验 >> 编程者的体验 >> 机器的体验

实际工作中,真正在打字的时间,远比阅读或者 debug 的时间要少。研究表明,软件工程中 80% 的时间都在阅读代码。 所以,为了提高效率,我们要优化的,不是你的打字时间,而是团队阅读的体验。


举例说明

命名规范

命名必须有意义,不能是无意义的单字母。

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 中规定,Python 代码中的 import 对象,只能是 package 或者 module 。

1
2
3
4
5
6
7
8
9
10
# 错误示例
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])
mypkg.my_func([1, 2, 3])

因为 my_func 这样的名字,如果没有一个 package name 提供上下文语境,读者很难单独通过 my_func 这个名字来推测它的可能功能,也很难在 debug 时根据 package name 找到可能的问题。

勿过度简化代码

代码应该写起来让人看着简洁明了,而非看着很累。

1
2
3
4
5
6
7
8
9
# 错误示范
result = [(x, y) for x in range(10) for y in range(5) if x * y > 20]

# 正确示范
result = []
for x in range(10):
for y in range(5):
if x * y > 20:
result.append((x, y))

机器体验

避免去用 is 比较两个 Python 整数的地址。

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

在 CPython 的实现中,把 -5 到 256 的整数做成了 singleton,也就是说,这个区间里的数字都会引用同一块内存区域,所以上面的 27 和下面的 27 会指向同一个地址,运行结果为 True。但是 -5 到 256 之外的数字,会因为你的重新定义而被重新分配内存,所以两个 721 会指向不同的内存地址,结果也就是 False 了。

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

和 None 比较时候永远使用 is 。

1
2
3
4
5
6
7
# 错误示范
x = MyObject()
print(x == None)

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

Python 中的隐式布尔转换

1
2
3
4
5
6
7
8
9
10
11
# 错误示范
def pay(salary=None):
if not salary:
salary = 1
print(salary)

# 正确示范
def pay(salary=None):
if salary is not None:
salary = 1
print(salary)

不规范的编程习惯也会导致程序效率问题

1
2
3
4
5
6
7
8
# 错误示例
adict = {i: i * 2 for i in range(100)}
for key in adict.keys():
print(f"{key}:{adict[key]}")

# 正确示范
for key in adict:
print(f"{key}:{adict[key]}")

keys() 方法会在遍历前生成一个临时的列表,导致上面的代码消耗大量内存并且运行缓慢。正确的方式,是使用默认的 iterator。默认的 iterator 不会分配新内存,也就不会造成上面的性能问题。


整合进开发流程的自动化工具

强制代码评审和强制静态或者动态 linter 。

在代码评审工具里,添加必须的编程规范环节;把团队确定的代码规范写进 Pylint 里(https://www.pylint.org/),能够在每份代码提交前自动检查,不通过的代码无法提交。


合理分解代码

编程中一个核心思想是,不写重复代码。重复代码大概率可以通过使用条件、循环、构造函数和类来解决。

另一个核心思想则是,减少迭代层数,尽可能让 Python 代码扁平化,毕竟,人的大脑无法处理过多的栈操作。

一个函数的粒度应该尽可能细,不要让一个函数做太多的事情。所以,对待一个复杂的函数,我们需要尽可能地把它拆分成几个功能简单的函数,然后合并起来。


合理利用 assert

Python 的 assert 语句,可以说是一个 debug 的好工具,主要用于测试一个条件是否满足。如果测试的条件满足,则什么也不做,相当于执行了 pass 语句;如果测试条件不满足,便会抛出异常 AssertionError,并返回具体的错误信息(optional)。

assert 的合理使用,可以增加代码的健壮度,同时也方便了程序出错后的定位排查。

但是,也不能滥用 assert。很多情况下,程序中出现的不同情况都是意料之中的,需要用不同的方案去处理,这时候用条件语句进行判断更为合适。而对于程序中的一些 run-time error,最好使用异常处理。


巧用上下文管理器和with精简代码

上下文管理器,能够自动分配并且释放资源,其中最典型的应用便是 with 语句。

1
2
3
4
5
6
7
8
9
with open('test.txt', 'w') as f:
f.write('hello')

# 相当于
f = open('test.txt', 'w')
try:
f.write('hello')
finally:
f.close()
1
2
3
4
5
6
7
8
9
10
11
some_lock = threading.Lock()
with somelock:
...

# 相当于
some_lock = threading.Lock()
some_lock.acquire()
try:
...
finally:
some_lock.release()

上下文管理器的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class FileManager:
def __init__(self, file_name, open_mode):
print("__init__")
self.file_name = file_name
self.open_mode = open_mode
self.file = None

def __enter__(self):
print("__enter__")
self.file = open(self.file_name, mode=self.open_mode)
return self.file

def __exit__(self, exc_type, exc_val, exc_tb):
print("__exit__")
if self.file:
self.file.close()


if __name__ == '__main__':
with FileManager('test.txt', 'w') as f:
f.write('aaa')

基于生成器的上下文管理器

使用装饰器 contextlib.contextmanager,来定义自己所需的基于生成器的上下文管理器,用以支持 with 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from contextlib import contextmanager

@contextmanager
def file_manager(file_name, open_mode):
f = None
try:
f = open(file_name, mode=open_mode)
yield f
finally:
if f:
f.close()

if __name__ == '__main__':
with file_manager('test.txt', 'w') as f:
f.write("file_manager")

函数 file_manager() 是一个生成器,当执行 with 语句时,便会打开文件,并返回文件对象 f;当 with 语句执行完后,finally block 中的关闭文件操作便会执行。

基于类的上下文管理器更加灵活,适用于大型的系统开发;而基于生成器的上下文管理器更加方便、简洁,适用于中小型程序。


单元测试

编写测试来验证某一个模块的功能正确性,一般会指定输入,验证输出是否符合预期。

实际生产环境中,会对每一个模块的所有可能输入值进行测试。这样虽然显得繁琐,增加了额外的工作量,但是能够大大提高代码质量,减小 bug 发生的可能性,也更方便系统的维护。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import unittest

# 将要被测试的排序函数
def sort(arr):
l = len(arr)
for i in range(0, l):
for j in range(i + 1, l):
if arr[i] >= arr[j]:
tmp = arr[i]
arr[i] = arr[j]
arr[j] = tmp

# 编写子类继承 unittest.TestCase
class TestSort(unittest.TestCase):
# 以 test 开头的函数将会被测试
def test_sort(self):
arr = [3, 4, 1, 5, 6]
sort(arr)
# assert 结果和期待的一样
self.assertEqual(arr, [1, 3, 4, 5, 6])

if __name__ == '__main__':
unittest.main()
mock

mock 是单元测试中最核心重要的一环。mock 的意思,便是通过一个虚假对象,来代替被测试函数或模块需要的对象。

比如要测一个后端 API 逻辑的功能性,但一般后端 API 都依赖于数据库、文件系统、网络等。这样,就需要通过 mock,来创建一些虚假的数据库层、文件系统层、网络层对象,以便可以简单地对核心后端逻辑单元进行测试。

Python mock 则主要使用 mock 或者 MagicMock 对象 ,举例:

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
import unittest
from unittest.mock import MagicMock

class A(unittest.TestCase):
def m1(self):
val = self.m2()
self.m3(val)

def m2(self):
print("m2")

def m3(self, val):
print("m3")

def test_m1(self):
a = A()
a.m2 = MagicMock(return_value="custom_val")
a.m3 = MagicMock()
a.m1()
self.assertTrue(a.m2.called) # 验证 m2 被 call 过
a.m3.assert_called_with("custom_val") # 验证 m3 被指定参数 call 过

if __name__ == '__main__':
# unittest.main(argv=['first-arg-is-ignored'], exit=False)
# unittest.main()
a = A()
a.test_m1()

这段代码中,定义了一个类的三个方法 m1()、m2()、m3()。需要对 m1() 进行单元测试,但是 m1() 取决于 m2() 和 m3()。如果 m2() 和 m3() 的内部比较复杂,就不能只是简单地调用 m1() 函数来进行测试,可能需要解决很多依赖项的问题。

但是,有了 mock 其实就很好办了。我们可以把 m2() 替换为一个返回具体数值的 value,把 m3() 替换为另一个 mock(空函数)。这样,测试 m1() 就很容易了,我们可以测试 m1() 调用 m2(),并且用 m2() 的返回值调用 m3()。

真正工业化的代码,都是很多层模块相互逻辑调用的一个树形结构。单元测试需要测的是某个节点的逻辑功能,mock 掉相关的依赖项是非常重要的。

Mock Side Effect

含义:mock 的函数,属性是可以根据不同的输入,返回不同的数值,而不只是一个 return_value 。

1
2
3
4
5
6
7
from unittest import mock

mock_obj = mock.Mock(side_effect=[1, 2, 3])
print(mock_obj())
print(mock_obj())
print(mock_obj())
# print(mock_obj()) # StopIteration
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
39
import unittest
from unittest import mock

class AddClass(object):
def add(self, a, b):
s = a + b
print(f"s is {s}")
return s

class TestAdd(unittest.TestCase):
def test_add1(self):
sub = AddClass()
sub.add = mock.Mock(return_value=10)
result = sub.add(1, 2)
self.assertEqual(result, 10) # 断言实际结果和预期结果

def test_add2(self):
sub = AddClass() # 初始化被测函数类实例
sub.add = mock.Mock(return_value=10,
side_effect=[1, 2, 3]) # 传递side_effect关键字参数, 会覆盖return_value参数值
result1 = sub.add(1, 10)
self.assertEqual(result1, 1) # test passed
result2 = sub.add(2, 10)
self.assertEqual(result2, 2) # test passed
result3 = sub.add(3, 10)
self.assertEqual(result3, 2) # test failed

def test_add3(self):
sub = AddClass()
sub.add = mock.Mock(return_value=10, side_effect=sub.add) # 使用真实的add方法测试
result1 = sub.add(1, 10)
self.assertEqual(result1, 11)
result2 = sub.add(2, 10)
self.assertEqual(result2, 12)
result3 = sub.add(3, 10)
self.assertEqual(result3, 13)

if __name__ == '__main__':
unittest.main()
patch

patch 可以应用 Python 的 decoration 模式或是 context manager 概念,快速自然地 mock 所需的函数。


调试代码

如何使用 pdb
1
2
3
4
5
6
7
8
9
10
11
12
13
import pdb

def func(a, b):
s = a + b
print('enter func()')
return s

a, b = 1, 2

pdb.set_trace()
ret = func(a, b)
c = 3
print(ret + c)
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
D:\Python_basis>python test_data.py
> d:\python_basis\test_data.py(13)<module>()
-> ret = func(a, b)
(Pdb) s
--Call--
> d:\python_basis\test_data.py(4)func()
-> def func(a, b):
(Pdb) l
1 import pdb
2
3
4 -> def func(a, b):
5 s = a + b
6 print('enter func()')
7 return s
8
9
10 a, b = 1, 2
11
(Pdb) n
> d:\python_basis\test_data.py(5)func()
-> s = a + b
(Pdb) p a
1
(Pdb) p b
2
(Pdb) n
> d:\python_basis\test_data.py(6)func()
-> print('enter func()')
(Pdb) p s
3
(Pdb) n
enter func()
> d:\python_basis\test_data.py(7)func()
-> return s
(Pdb) n
--Return--
> d:\python_basis\test_data.py(7)func()->3
-> return s
(Pdb) n
> d:\python_basis\test_data.py(14)<module>()
-> c = 3
(Pdb) p ret
3
(Pdb) n
> d:\python_basis\test_data.py(15)<module>()
-> print(ret + c)
(Pdb) n
6
--Return--
> d:\python_basis\test_data.py(15)<module>()->None
-> print(ret + c)

pdf 官方文档:https://docs.python.org/3/library/pdb.html#modulepdb


用 cProfile 进行性能分析

profile,是指对代码的每个部分进行动态的分析,比如准确计算出每个模块消耗的时间等。这样你就可以知道程序的瓶颈所在,从而对其进行修正或优化 。

方式一:在代码中加入 import cProfile,并将执行语句放入 cProfile.run()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import cProfile

def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n - 1) + fib(n - 2)

def fib_seq(n):
res = []
if n > 0:
res.extend(fib_seq(n - 1))
res.append(fib(n))
return res

cProfile.run('fib_seq(30)')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
D:\compile\Python3.6.0\python.exe D:/Python_basis/test_data.py
7049216 function calls (94 primitive calls) in 6.177 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 6.177 6.177 <string>:1(<module>)
31/1 0.000 0.000 6.177 6.177 test_data.py:13(fib_seq)
7049122/30 6.177 0.000 6.177 0.206 test_data.py:4(fib)
1 0.000 0.000 6.177 6.177 {built-in method builtins.exec}
30 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
30 0.000 0.000 0.000 0.000 {method 'extend' of 'list' objects}

Process finished with exit code 0

方式二:直接在运行脚本的命令中,加入选项 -m cProfile 。不用在脚本中加入 import CProfile 等了。

1
python3 -m cProfile test_data.py

方式三:直接使用 Pycharm 中的 Run-Profile 执行脚本。

参数解析

  • ncalls,是指相应代码 / 函数被调用的次数;
  • tottime,是指对应代码 / 函数总共执行所需要的时间(注意,并不包括它调用的其他代码 / 函数的执行时间);
  • tottime percall,就是上述两者相除的结果,也就是 tottime / ncalls;
  • cumtime,则是指对应代码 / 函数总共执行所需要的时间,这里包括了它调用的其他代码 / 函数的执行时间;
  • cumtime percall,则是 cumtime 和 ncalls 相除的平均结果。

通过解析发现,fib 函数被调用次数太多,7049122次,需要改进。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 改进版
from functools import lru_cache

@lru_cache()
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n - 1) + fib(n - 2)

def fib_seq(n):
res = []
if n > 0:
res.extend(fib_seq(n - 1))
res.append(fib(n))
return res

fib_seq(30)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
D:\Python_basis>python -m cProfile test_data.py
144 function calls (113 primitive calls) in 0.000 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:989(_handle_fromlist)
1 0.000 0.000 0.000 0.000 functools.py:44(update_wrapper)
1 0.000 0.000 0.000 0.000 functools.py:449(lru_cache)
1 0.000 0.000 0.000 0.000 functools.py:480(decorating_function)
1 0.000 0.000 0.000 0.000 test_data.py:1(<module>)
31/1 0.000 0.000 0.000 0.000 test_data.py:14(fib_seq)
31/30 0.000 0.000 0.000 0.000 test_data.py:4(fib)
1 0.000 0.000 0.000 0.000 {built-in method builtins.exec}
7 0.000 0.000 0.000 0.000 {built-in method builtins.getattr}
1 0.000 0.000 0.000 0.000 {built-in method builtins.hasattr}
1 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}
5 0.000 0.000 0.000 0.000 {built-in method builtins.setattr}
30 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
30 0.000 0.000 0.000 0.000 {method 'extend' of 'list' objects}
1 0.000 0.000 0.000 0.000 {method 'update' of 'dict' objects}

常见问题

异常处理
  • 在代码中对数据进行检测,并直接处理与抛出异常。

    1
    2
    3
    4
    if [condition1]:
    raise Exception1('exception 1')
    elif [condition2]:
    raise Exception2('exception 2')

    一旦抛出异常,那么程序就会终止。

  • 在异常处理代码中进行处理。

    1
    2
    3
    4
    try:
    do something
    except Exception as e:
    other

    如果抛出异常,会被程序捕获(catch),程序还会继续运行。


代码优化时间

先写出能跑起来的代码,后期再优化。很明显,这种认知是错误的。

从一开始写代码时,就必须对功能和规范这两者双管齐下。

代码功能完整和规范完整的优先级是不分先后的,应该是同时进行的。如果你一开始只注重代码的功能完整,而不关注其质量、规范,那么规范问题很容易越积越多。这样就会导致产品的 bug 越来越多,相应的代码库越发难以维护,到最后不得已只能推倒重来。


代码中写多少注释合适

通常来说,会在类、函数的开头或者是某一个功能块的开头加上一段描述性的注释,来说明这段代码的功能,并指明所有的输入和输出。

除此之外,也要求在一些比较复杂、可能有歧义的代码上方加上注释,帮助阅读者理解代码的含义。

在写好之后修改了代码,代码对应的注释一定也要做出相应的修改,不然很容易造成“文不对题”的现象,给别人也给你自己带来困扰。