- A+
Python已经演化出了一个广泛的生态系统,该生态系统能够让Python程序员的生活变得更加简单,减少他们重复造轮的工作。同样的理念也适用于工具开发者的工作,即便他们开发出的工具并没有出现在最终的程序中。本文将介绍Python程序员必知必会的开发者工具。
对于开发者来说,最实用的帮助莫过于帮助他们编写代码文档了。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程序中有意思的字符进行扫描,比如函数或者是类。在提供了类查看器以后,就无需引入代码,这样就避免了潜在的副作用影响。
文档字符串与doctest模块
如果函数,类或者是模块的第一行是一个字符串,那么这个字符串就是一个文档字符串。可以认为包含文档字符串是一个良好的编程习惯,这是因为这些字符串可以给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) # Trying: # a=Test(5) # Expecting nothing # ok # Trying: # a.multiply_by_2() # Expecting: # 10 # ok # 3 items had no tests: # mult # mult.Test.__init__ # mult.Test.multiply_by_2 # 1 items passed all tests: # 2 tests in mult.Test # 2 tests in 4 items. # 2 passed and 0 failed. # Test passed.
在这段代码中,doctest.testmod(module)会执行特定模块的测试,并且返回测试失败的个数以及测试的总数目。如果所有的测试都通过了,那么不会产生任何输出。否则的话,你将会看到一个失败报告,用来显示期望值和实际值之间的差别。如果你想看到测试的详细输出,你可以使用testmod(module, verbose=True).
如果不想新建一个单独的测试文件的话,那么另一种选择就是在文件末尾包含相应的测试代码:
if __name__ == '__main__': import doctest doctest.testmod()
如果想执行这类测试的话,我们可以通过-m选项调用doctest模块。通常来讲,当执行测试的时候没有任何的输出。如果想查看详细信息的话,可以加上-v选项。
$ python -m doctest -v mult.py
单元测试与unittest模块
如果想更加彻底地对程序进行测试,我们可以使用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() #... #---------------------------------------------------------------------- #Ran 3 tests in 0.001s #OK
在使用单元测试时,我们需要定义一个继承自unittest.TestCase的类。在这个类里面,每一个测试都以方法的形式进行定义,并都以test打头进行命名——例如,’testsimplestring‘,’testtypeconvert‘以及类似的命名方式(有必要强调一下,只要方法名以test打头,那么无论怎么命名都是可以的)。在每个测试中,断言可以用来对不同的条件进行检查。
实际的例子:
假如你在程序里有一个方法,这个方法的输出指向标准输出(sys.stdout)。这通常意味着是往屏幕上输出文本信息。如果你想对你的代码进行测试来证明这一点,只要给出相应的输入,那么对应的输出就会被显示出来。
# url.py def urlprint(protocol, host, domain): url = '{}://{}.{}'.format(protocol, host, domain) print(url)
内置的print函数在默认情况下会往sys.stdout发送输出。为了测试输出已经实际到达,你可以使用一个替身对象对其进行模拟,并且对程序的期望值进行断言。unittest.mock模块中的patch()方法可以只在运行测试的上下文中才替换对象,在测试完成后就立刻返回对象原始的状态。下面是urlprint()方法的测试代码:
#urltest.py from io import StringIO from unittest import TestCase from unittest.mock import patch import url class TestURLPrint(TestCase): def test_url_gets_to_stdout(self): protocol = 'http' host = 'www' domain = 'example.com' expected_url = '{}://{}.{}\n'.format(protocol, host, domain) with patch('sys.stdout', new=StringIO()) as fake_out: url.urlprint(protocol, host, domain) self.assertEqual(fake_out.getvalue(), expected_url)
urlprint()函数有三个参数,测试代码首先给每个参数赋了一个假值。变量expected_url包含了期望的输出字符串。为了能够执行测试,我们使用了unittest.mock.patch()方法作为上下文管理器,把标准输出sys.stdout替换为了StringIO对象,这样发送的标准输出的内容就会被StringIO对象所接收。变量fake_out就是在这一过程中所创建出的模拟对象,该对象能够在with所处的代码块中所使用,来进行一系列的测试检查。当with语句完成时,patch方法能够将所有的东西都复原到测试执行之前的状态,就好像测试没有执行一样,而这无需任何额外的工作。但对于某些Python的C扩展来讲,这个例子却显得毫无意义,这是因为这些C扩展程序绕过了sys.stdout的设置,直接将输出发送到了标准输出上。这个例子仅适用于纯Python代码的程序(如果你想捕获到类似C扩展的输入输出,那么你可以通过打开一个临时文件然后将标准输出重定向到该文件的技巧来进行实现)。