对于开发者来说,最实用的帮助莫过于帮助他们编写代码文档了。pydoc模块可以根据源代码中的docstrings为任何可导入模块生成格式良好的文档。Python包含了两个测试框架来自动测试代码以及验证代码的正确性:1)doctest模块,该模块可以从源代码或独立文件的例子中抽取出测试用例。2)unittest模块,该模块是一个全功能的自动化测试框架,该框架提供了对测试准备(test fixtures), 预定义测试集(predefined test suite)以及测试发现(test discovery)的支持。
trace模块可以监控Python执行程序的方式,同时生成一个报表来显示程序的每一行执行的次数。这些信息可以用来发现未被自动化测试集所覆盖的程序执行路径,也可以用来研究程序调用图,进而发现模块之间的依赖关系。编写并执行测试可以发现绝大多数程序中的问题,Python使得debug工作变得更加简单,这是因为在大部分情况下,Python都能够将未被处理的错误打印到控制台中,我们称这些错误信息为traceback。如果程序不是在文本控制台中运行的,traceback也能够将错误信息输出到日志文件或是消息对话框中。当标准的traceback无法提供足够的信息时,可以使用cgitb 模块来查看各级栈和源代码上下文中的详细信息,比如局部变量。cgitb模块还能够将这些跟踪信息以HTML的形式输出,用来报告web应用中的错误。
一旦发现了问题出在哪里后,就需要使用到交互式调试器进入到代码中进行调试工作了,pdb模块能够很好地胜任这项工作。该模块可以显示出程序在错误产生时的执行路径,同时可以动态地调整对象和代码进行调试。当程序通过测试并调试后,下一步就是要将注意力放到性能上了。开发者可以使用profile以及timit模块来测试程序的速度,找出程序中到底是哪里很慢,进而对这部分代码独立出来进行调优的工作。Python程序是通过解释器执行的,解释器的输入是原有程序的字节码编译版本。这个字节码编译版本可以在程序执行时动态地生成,也可以在程序打包的时候就生成。compileall模块可以处理程序打包的事宜,它暴露出了打包相关的接口,该接口能够被安装程序和打包工具用来生成包含模块字节码的文件。同时,在开发环境中,compileall模块也可以用来验证源文件是否包含了语法错误。
在源代码级别,pyclbr模块提供了一个类查看器,方便文本编辑器或是其他程序对Python程序中有意思的字符进行扫描,比如函数或者是类。在提供了类查看器以后,就无需引入代码,这样就避免了潜在的副作用影响。
如果函数,类或者是模块的第一行是一个字符串,那么这个字符串就是一个文档字符串。可以认为包含文档字符串是一个良好的编程习惯,这是因为这些字符串可以给Python程序开发工具提供一些信息。比如,help()命令能够检测文档字符串,Python相关的IDE也能够进行检测文档字符串的工作。由于程序员倾向于在交互式shell中查看文档字符串,所以最好将这些字符串写的简短一些。例如
# mult.py class Test: """ >>> a=Test(5) >>> a.multiply_by_2() 10 """ def __init__(self, number): self._number=number def multiply_by_2(self): return self._number*2在编写文档时,一个常见的问题就是如何保持文档和实际代码的同步。例如,程序员也许会修改函数的实现,但是却忘记了更新文档。针对这个问题,我们可以使用doctest模块。doctest模块收集文档字符串,并对它们进行扫描,然后将它们作为测试进行执行。为了使用doctest模块,我们通常会新建一个用于测试的独立的模块。例如,如果前面的例子 Test class 包含在文件 mult.py 中,那么,你应该新建一个 testmult.py 文件用来测试,如下所示:
# testmult.py import mult, doctest doctest.testmod(mult, verbose=True)
在这段代码中,doctest.testmod(module)会执行特定模块的测试,并且返回测试失败的个数以及测试的总数目。如果所有的测试都通过了,那么不会产生任何输出。否则的话,你将会看到一个失败报告,用来显示期望值和实际值之间的差别。如果你想看到测试的详细输出,你可以使用testmod(module, verbose=True).
如果不想新建一个单独的测试文件的话,那么另一种选择就是在文件末尾包含相应的测试代码:
if __name__ == '__main__': import doctest doctest.testmod()如果想执行这类测试的话,我们可以通过-m选项调用doctest模块。通常来讲,当执行测试的时候没有任何的输出。如果想查看详细信息的话,可以加上-v选项。
python -m doctest -v mult.py
如果想更加彻底地对程序进行测试,我们可以使用unittest模块。通过单元测试,开发者可以为构成程序的每一个元素(例如,独立的函数,方法,类以及模块)编写一系列独立的测试用例。当测试更大的程序时,这些测试就可以作为基石来验证程序的正确性。当我们的程序变得越来越大的时候,对不同构件的单元测试就可以组合起来成为更大的测试框架以及测试工具。这能够极大地简化软件测试的工作,为找到并解决软件问题提供了便利。
# splitter.py import unittest def split(line, types=None, delimiter=None): """ Splits a line of text and optionally performs type conversion. """ fields = line.split(delimiter) if types: fields = [ ty(val) for ty,val in zip(types,fields) ] return fields class TestSplitFunction(unittest.TestCase): def setUp(self): # Perform set up actions (if any) pass def tearDown(self): # Perform clean-up actions (if any) pass def testsimplestring(self): r = split('GOOG 100 490.50') self.assertEqual(r,['GOOG','100','490.50']) def testtypeconvert(self): r = split('GOOG 100 490.50',[str, int, float]) self.assertEqual(r,['GOOG', 100, 490.5]) def testdelimiter(self): r = split('GOOG,100,490.50',delimiter=',') self.assertEqual(r,['GOOG','100','490.50']) # Run the unittests if __name__ == '__main__': unittest.main()
qixuan@ubuntu:~/qixuan02$ python splitter.py ... ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK在使用单元测试时,我们需要定义一个继承自 unittest.TestCase 的类。在这个类里面,每一个测试都以方法的形式进行定义,并都以 test 打头进行命名——例如,’ testsimplestring ‘,’ testtypeconvert ‘以及类似的命名方式(有必要强调一下,只要方法名以 test 打头,那么无论怎么命名都是可以的)。在每个测试中,断言可以用来对不同的条件进行检查。
Python在pdb模块中包含了一个简单的基于命令行的调试器。pdb模块支持事后调试,栈帧探查,断点,单步调试以及代码审查。能够在程序中调用调试器,或是在交互式的Python终端中进行调试工作。在所有启动调试器的函数中,函数set_trace()也许是最简易实用的了。如果在复杂程序中发现了问题,可以在代码中插入set_trace()函数,并运行程序。当执行到set_trace()函数时,这就会暂停程序的执行并直接跳转到调试器中,这时候你就可以大展手脚开始检查运行时环境了。当退出调试器时,调试器会自动恢复程序的执行。
假设你的程序有问题,你想找到一个简单的方法来对它进行调试。
如果你的程序崩溃时报了一个异常错误,那么你可以用python3 -i someprogram.py这个命令来运行你的程序,这能够很好地发现问题所在。-i选项表明只要程序终结就立即启动一个交互式shell。在这个交互式shell中,你就可以很好地探查到底发生了什么导致程序的错误。例如,如果你有以下代码:
def function(n): return n + 10 function("Hello")
qixuan@ubuntu:~/qixuan02$ python3 -i sample.py Traceback (most recent call last): File "sample.py", line 4, in <module> function("Hello") File "sample.py", line 2, in function return n + 10 TypeError: Can't convert 'int' object to str implicitly如 果你没有发现什么明显的错误,那么你可以进一步地启动Python调试器
>>> import pdb >>> pdb.pm() > /home/qixuan/qixuan02/sample.py(2)function() -> return n + 10 (Pdb) w /home/qixuan/qixuan02/sample.py(4)<module>() -> function("Hello") > /home/qixuan/qixuan02/sample.py(2)function() -> return n + 10 (Pdb) print (n) Hello (Pdb) q如 果你的代码身处的环境很难启动一个交互式shell的话(比如在服务器环境下),你可以增加错误处理的代码,并自己输出跟踪信息
profile模块和cProfile模块可以用来分析程序。它们的工作原理都一样,唯一的区别是,cProfile模块是以C扩展的方式实现的,如此一来运行的速度也快了很多,也显得比较流行。这两个模块都可以用来收集覆盖信息(比如,有多少函数被执行了),也能够收集性能数据。对一个程序进行分析的最简单的方法就是运行这个命令:python -m cProfile someprogram.py
此外,也可以使用profile模块中的run函数:run(command [, filename])该函数会使用 exec 语句执行command中的内容。filename是可选的文件保存名,如果没有filename的话,该命令的输出会直接发送到标准输出上。下面是分析器执行完成时的输出报告:
qixuan@ubuntu:~/qixuan02$ python3 -m cProfile splitter.py ---------------------------------------------------------------------- Ran 0 tests in 0.000s OK 12340 function calls (12059 primitive calls) in 0.208 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 22 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:1000(__init__) 20 0.000 0.000 0.002 0.000 <frozen importlib._bootstrap>:1019(init_module_attrs) 20 0.000 0.000 0.003 0.000 <frozen importlib._bootstrap>:1099(create) 20/1 0.000 0.000 0.197 0.197 <frozen importlib._bootstrap>:1122(_exec) 2 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:1156(_load_backward_compatible) 22/1 0.001 0.000 0.197 0.197 <frozen importlib._bootstrap>:1186(_load_unlocked) 22 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:1243(find_spec) 2 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:1264(load_module)
当输出中的第一列包含了两个数字时(比如,121/1),后者是元调用(primitive call)的次数,前者是实际调用的次数(译者注:只有在递归情况下,实际调用的次数才会大于元调用的次数,其他情况下两者都相等)。对于绝大部分的应用程序来讲使用该模块所产生的的分析报告就已经足够了,比如,你只是想简单地看一下你的程序花费了多少时间。然后,如果你还想将这些数据保存下来,并在将来对其进行分析,你可以使用pstats模块。
假设你想知道你的程序究竟在哪里花费了多少时间。如果你只是想简单地给你的整个程序计时的话,使用Unix中的time命令就已经完全能够应付了。例如:
qixuan@ubuntu:~/qixuan02$ time python3 splitter.py ... ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK real 0m0.379s user 0m0.216s sys 0m0.076s通常来讲,分析代码的程度会介于这两个极端之间。比如,你可能已经知道你的代码会在一些特定的函数中花的时间特别多。针对这类特定函数的分析,我们可以使用修饰器decorator。 使用decorator的方式很简单,你只需要把它放在你想要分析的函数的定义前面就可以了
qixuan@ubuntu:~/qixuan02$ python3 Python 3.4.0 (default, Jun 19 2015, 14:20:21) [GCC 4.8.2] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import time >>> from functools import wraps >>> ... def timethis(func): ... @wraps(func) ... def wrapper(*args, **kwargs): ... start = time.perf_counter() ... r = func(*args, **kwargs) ... end = time.perf_counter() ... print('{}.{} : {}'.format(func.__module__, func.__name__, end - start)) ... return r ... return wrapper ... >>> @timethis ... def countdown(n): ... while n > 0: ... n -= 1 ... >>> countdown(1000000) __main__.countdown : 0.24070458200003486
>>> import time >>> from contextlib import contextmanager >>> ... @contextmanager ... def timeblock(label): ... start = time.perf_counter() ... try: ... yield ... finally: ... end = time.perf_counter() ... print('{} : {}'.format(label, end - start)) ... >>> with timeblock('counting'): ... n = 1000000 ... while n > 0: ... n -= 1 ... counting : 0.4405263909993664如果想研究一小段代码的性能的话,timeit模块会非常有用。例如
>>> from timeit import timeit >>> timeit('math.sqrt(2)', 'import math') 0.46812720200068725timeit 的工作原理是,将第一个参数中的语句执行100万次,然后计算所花费的时间。第二个参数指定了一些测试之前需要做的环境准备工作。如果你需要改变迭代的次数,可以附加一个number参数,就像这样
>>> timeit('math.sqrt(2)', 'import math', number=10000000) 4.001195195000037当进行性能评估的时候,要牢记任何得出的结果只是一个估算值。函数 time.perf_counter() 能够在任一平台提供最高精度的计时器。然而,它也只是记录了自然时间,记录自然时间会被很多其他因素影响,比如,计算机的负载。如果你对处理时间而非自然时间感兴趣的话,你可以使用 time.process_time()
import time from functools import wraps def timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.process_time() r = func(*args, **kwargs) end = time.process_time() print('{}.{} : {}'.format(func.__module__, func.__name__, end - start)) return r return wrapperprofile模块中最基础的东西就是run()函数了。该函数会把一个语句字符串作为参数,然后在执行语句时生成所花费的时间报告。
import profile def fib(n): # from literateprograms.org # http://bit.ly/hlOQ5m if n == 0: return 0 elif n == 1: return 1 else: return fib(n-1) + fib(n-2) def fib_seq(n): seq = [] if n > 0: seq.extend(fib_seq(n-1)) seq.append(fib(n)) return seq profile.run('print(fib_seq(20)); print')
qixuan@ubuntu:~/qixuan02$ python3 abc.py [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765] 57358 function calls (68 primitive calls) in 0.418 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 21 0.000 0.000 0.000 0.000 :0(append) 1 0.000 0.000 0.375 0.375 :0(exec) 20 0.000 0.000 0.000 0.000 :0(extend) 1 0.000 0.000 0.000 0.000 :0(print) 1 0.043 0.043 0.043 0.043 :0(setprofile) 1 0.000 0.000 0.374 0.374 <string>:1(<module>) 21/1 0.000 0.000 0.374 0.374 abc.py:14(fib_seq) 57291/21 0.374 0.000 0.374 0.018 abc.py:2(fib) 1 0.000 0.000 0.418 0.418 profile:0(print(fib_seq(20)); print) 0 0.000 0.000 profile:0(profiler)