规范参考
统一的编程规范的重要性 统一的编程规范能提高开发效率。而开发效率,关乎三类对象,也就是阅读者、编程者和机器。
他们的优先级是 阅读者的体验 >> 编程者的体验 >> 机器的体验。
实际工作中,真正在打字的时间,远比阅读或者 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 Objfrom mypkg import my_funcmy_func([1 , 2 , 3 ]) import numpy as npimport mypkgnp.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 unittestdef 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 class TestSort (unittest.TestCase): def test_sort (self ): arr = [3 , 4 , 1 , 5 , 6 ] sort(arr) 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 unittestfrom unittest.mock import MagicMockclass 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) a.m3.assert_called_with("custom_val" ) if __name__ == '__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 mockmock_obj = mock.Mock(side_effect=[1 , 2 , 3 ]) print (mock_obj())print (mock_obj())print (mock_obj())
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 unittestfrom unittest import mockclass 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 ]) result1 = sub.add(1 , 10 ) self .assertEqual(result1, 1 ) result2 = sub.add(2 , 10 ) self .assertEqual(result2, 2 ) result3 = sub.add(3 , 10 ) self .assertEqual(result3, 2 ) def test_add3 (self ): sub = AddClass() sub.add = mock.Mock(return_value=10 , side_effect=sub.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 pdbdef 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 cProfiledef 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 }
常见问题 异常处理
代码优化时间 先写出能跑起来的代码,后期再优化。很明显,这种认知是错误的。
从一开始写代码时,就必须对功能和规范这两者双管齐下。
代码功能完整和规范完整的优先级是不分先后的,应该是同时进行的。如果你一开始只注重代码的功能完整,而不关注其质量、规范,那么规范问题很容易越积越多。这样就会导致产品的 bug 越来越多,相应的代码库越发难以维护,到最后不得已只能推倒重来。
代码中写多少注释合适 通常来说,会在类、函数的开头或者是某一个功能块的开头加上一段描述性的注释,来说明这段代码的功能,并指明所有的输入和输出。
除此之外,也要求在一些比较复杂、可能有歧义的代码上方加上注释,帮助阅读者理解代码的含义。
在写好之后修改了代码,代码对应的注释一定也要做出相应的修改,不然很容易造成“文不对题”的现象,给别人也给你自己带来困扰。