文章目录
规范参考
- 《8 号 Python 增强规范》(Python Enhacement Proposal #8),简称 PEP8;
- 《Google Python 风格规范》(Google Python Style Guide),源自 Google 内部的风格规范。
统一的编程规范的重要性
统一的编程规范能提高开发效率。而开发效率,关乎三类对象,也就是阅读者、编程者和机器。
他们的优先级是 阅读者的体验 >> 编程者的体验 >> 机器的体验
。
实际工作中,真正在打字的时间,远比阅读或者 debug 的时间要少。研究表明,软件工程中 80% 的时间都在阅读代码。 所以,为了提高效率,我们要优化的,不是你的打字时间,而是团队阅读的体验。
举例说明
命名规范
命名必须有意义,不能是无意义的单字母。
# 错误示例
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 。
# 错误示例
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 找到可能的问题。
勿过度简化代码
代码应该写起来让人看着简洁明了,而非看着很累。
# 错误示范
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 整数的地址。
# 错误示例
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 了。
# 正确示例
x = 27
y = 27
print(x == y)
x = 721
y = 721
print(x == y)
和 None 比较时候永远使用 is 。
# 错误示范
x = MyObject()
print(x == None)
# 正确示例
x = MyObject()
print(x is None)
Python 中的隐式布尔转换
# 错误示范
def pay(salary=None):
if not salary:
salary = 1
print(salary)
# 正确示范
def pay(salary=None):
if salary is not None:
salary = 1
print(salary)
不规范的编程习惯也会导致程序效率问题
# 错误示例
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 语句。
with open('test.txt', 'w') as f:
f.write('hello')
# 相当于
f = open('test.txt', 'w')
try:
f.write('hello')
finally:
f.close()
some_lock = threading.Lock()
with somelock:
...
# 相当于
some_lock = threading.Lock()
some_lock.acquire()
try:
...
finally:
some_lock.release()
上下文管理器的例子
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 语句。
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 发生的可能性,也更方便系统的维护。
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 对象 ,举例:
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 。
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
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
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)
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()
。
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)')
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
等了。
python3 -m cProfile test_data.py
方式三:直接使用 Pycharm 中的 Run-Profile
执行脚本。
参数解析
- ncalls,是指相应代码 / 函数被调用的次数;
- tottime,是指对应代码 / 函数总共执行所需要的时间(注意,并不包括它调用的其他代码 / 函数的执行时间);
- tottime percall,就是上述两者相除的结果,也就是 tottime / ncalls;
- cumtime,则是指对应代码 / 函数总共执行所需要的时间,这里包括了它调用的其他代码 / 函数的执行时间;
- cumtime percall,则是 cumtime 和 ncalls 相除的平均结果。
通过解析发现,fib 函数被调用次数太多,7049122次,需要改进。
# 改进版
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)
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}
常见问题
异常处理
-
在代码中对数据进行检测,并直接处理与抛出异常。
if [condition1]: raise Exception1('exception 1') elif [condition2]: raise Exception2('exception 2')
一旦抛出异常,那么程序就会终止。
-
在异常处理代码中进行处理。
try: do something except Exception as e: other
如果抛出异常,会被程序捕获(catch),程序还会继续运行。
代码优化时间
先写出能跑起来的代码,后期再优化。很明显,这种认知是错误的。
从一开始写代码时,就必须对功能和规范这两者双管齐下。
代码功能完整和规范完整的优先级是不分先后的,应该是同时进行的。如果你一开始只注重代码的功能完整,而不关注其质量、规范,那么规范问题很容易越积越多。这样就会导致产品的 bug 越来越多,相应的代码库越发难以维护,到最后不得已只能推倒重来。
代码中写多少注释合适
通常来说,会在类、函数的开头或者是某一个功能块的开头加上一段描述性的注释,来说明这段代码的功能,并指明所有的输入和输出。
除此之外,也要求在一些比较复杂、可能有歧义的代码上方加上注释,帮助阅读者理解代码的含义。
在写好之后修改了代码,代码对应的注释一定也要做出相应的修改,不然很容易造成“文不对题”的现象,给别人也给你自己带来困扰。