Day-10: 错误、调试和测试
程序运行时,会遇到各种各样的错误。
编写错误叫做bug,而另一类由于运行过程中无法预测的,比如写文件时,磁盘满了,写不进去;或者从网络抓取数据,网络突然掉了。这些错误称为异常,程序中需要对异常进行处理,使得程序能够运行下去。
- 错误处理
Python中,程序运行错误时,如果错误没有捕获,它会一直往上抛,最后被Python解释器捕获,打印一个错误。
# err.py: def foo(s): return 10 / int(s) def bar(s): return foo(s) * 2 def main(): bar('0') main()
$ python err.py Traceback (most recent call last): File "err.py", line 11, in <module> main() File "err.py", line 9, in main bar('0') File "err.py", line 6, in bar return foo(s) * 2 File "err.py", line 3, in foo return 10 / int(s) ZeroDivisionError: integer division or modulo by zero
从上到下,错误会一层层的反馈,直到显示最终出错的地方。
try...except...finally...:常用这种方法来检查错误并捕捉到,同时进行相应的处理。
try: print 'try...' r = 10 / 0 print 'result:', r except ZeroDivisionError, e: print 'except:', e finally: print 'finally...' print 'END'
try... except: integer division or modulo by zero finally... END
注意到,错误类型有很多种,它们其实都是从BaseException类派生出来的,常见的错误类型和继承关系有:https://docs.python.org/2/library/exceptions.html#exception-hierarchy
- 调试
程序运行一次就成功的概率很小,基本上不超过1%。一般有如下的调试方法:
第一种,直接在可能出错的地方print出来,但是后期会一个个删掉。
第二种,使用断言来代替。
# err.py def foo(s): n = int(s) assert n != 0, 'n is zero!' return 10 / n def main(): foo('0')
断言中,如果“n != 0”是错的,就抛出AssertionError,并显示后面的字符串。
第三种,使用logging。
logging有debug,info,warning,error等几个级别,从前到后优先级依次提高,即如果,指定level=WARNING后,debug和info就不起作用了。这样一样,就可以输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,,比如console和文件。
# err.py import logging
logging.basicConfig(level=logging.INFO) s = '0' n = int(s) logging.info('n = %d' % n) print 10 / n
$ python err.py INFO:root:n = 0 Traceback (most recent call last): File "err.py", line 8, in <module> print 10 / n ZeroDivisionError: integer division or modulo by zero
第四种调试方式,就是调试器pdb,让程序以单步方式运行,可以随时查看运行状态。
# err.py s = '0' n = int(s) print 10 / n
然后,以参数-m pdb启动,单步运行
$ python -m pdb err.py > /Users/michael/Github/sicp/err.py(2)<module>() -> s = '0'
(Pdb) l 1 # err.py 2 -> s = '0' 3 n = int(s) 4 print 10 / n [EOF]
输入n单步运行下一步
(Pdb) n > /Users/michael/Github/sicp/err.py(3)<module>() -> n = int(s) (Pdb) n > /Users/michael/Github/sicp/err.py(4)<module>() -> print 10 / n
输入p 变量名来查看变量状态。
(Pdb) p s '0' (Pdb) p n 0
输入q结束运行
(Pdb) n ZeroDivisionError: 'integer division or modulo by zero' > /Users/michael/Github/sicp/err.py(4)<module>() -> print 10 / n (Pdb) q
另外在合适的地方,设置pdb.set_trace(),可以作为断点。
# err.py import pdb s = '0' n = int(s) pdb.set_trace() # 运行到这里会自动暂停 print 10 / n
$ python err.py > /Users/michael/Github/sicp/err.py(7)<module>() -> print 10 / n (Pdb) p n 0 (Pdb) c Traceback (most recent call last): File "err.py", line 7, in <module> print 10 / n ZeroDivisionError: integer division or modulo by zero
到达断点时,进入pdb调试器。
最后,还有方便的IDE调试器。
- 单元测试
单元测试,顾名思义,就是对一个部分测试,可以是一个模块、一个函数或者一个类。它的目的是保证该单元能够实现原先规划的功能,为之后的整体调试做准备。
例如,现有模块mydict.py,对它的要求是实现如下功能:
>>> d = Dict(a=1, b=2) >>> d['a'] 1 >>> d.a 1
mydict.py代码如下:
class Dict(dict): def __init__(self, **kw): super(Dict, self).__init__(**kw) def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(r"'Dict' object has no attribute '%s'" % key) def __setattr__(self, key, value): self[key] = value
编写的单元测试,需要引入unittest模块,编写的mydict_test.py如下:
import unittest from mydict import Dict class TestDict(unittest.TestCase): def test_init(self): # 测试初始化功能 d = Dict(a=1, b='test') self.assertEquals(d.a, 1) self.assertEquals(d.b, 'test') self.assertTrue(isinstance(d, dict)) def test_key(self): # 测试key的功能 d = Dict() d['key'] = 'value' self.assertEquals(d.key, 'value') def test_attr(self): # 测试属性功能 d = Dict() d.key = 'value' self.assertTrue('key' in d) self.assertEquals(d['key'], 'value') def test_keyerror(self): # 测试key错误的功能 d = Dict() with self.assertRaises(KeyError): value = d['empty'] def test_attrerror(self): # 测试属性错误的功能 d = Dict() with self.assertRaises(AttributeError): value = d.empty
编写的单元测试类,从unittest.TestCase继承。其中,只有以test开头的方法是测试方法。
运行单元测试时,可以在测试文件中加入:
if __name__ == '__main__': unittest.main()
然后run。
另一种,在命令行中输入命令:
$ python -m unittest mydict_test ..... ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK
第二种方法,可以一次运行多个测试文件,比较方便。
setUp与tearDown:在每个测试方法前后分别被执行,避免在测试代码中重复加入代码。
最后,单元测试要考虑到异常,代码不能过于复杂,以免本身就有bug。
- 文档测试
Python中可以提供实例文档,在文件中编写特定格式的注释,调用doctest判断程序是否会像注释中那样的运行。
class Dict(dict): ''' Simple dict but also support access as x.y style. >>> d1 = Dict() >>> d1['x'] = 100 >>> d1.x 100 >>> d1.y = 200 >>> d1['y'] 200 >>> d2 = Dict(a=1, b=2, c='3') >>> d2.c '3' >>> d2['empty'] Traceback (most recent call last): ... KeyError: 'empty' >>> d2.empty Traceback (most recent call last): ... AttributeError: 'Dict' object has no attribute 'empty' ''' def __init__(self, **kw): super(Dict, self).__init__(**kw) def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(r"'Dict' object has no attribute '%s'" % key) def __setattr__(self, key, value): self[key] = value if __name__=='__main__': import doctest doctest.testmod()
然后run。如果什么都没输出,就说明编写的doctest运行都是正确的。
注:本文为学习廖雪峰Python入门整理后的笔记