二、调试:
1. print
三、单元测试 :是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
四、文档测试 :(doctest) 直接提取注释中的代码并执行测试.
-------------------------------------------------------------------------------------------------------------------
某些代码可能会出错时,就可以用try
来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except
语句块,执行完except
后,如果有finally
语句块,则执行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
没有错误发生,所以except
语句块不会被执行,但是finally
如果有,则一定会被执行(可以没有finally
语句).
错误应该有很多种类,如果发生了不同类型的错误,应该由不同的except
语句块处理。可以有多个except
来捕获不同类型的错误:
>>> try: ... print 'try...' ... r = 10 / int('a') ... print 'result:' ,r ... except ValueError,e: ... print 'ValueError:',e ... except ZeroDivisionError,e: ... print 'ZeroDivisonError',e ... finally: ... print 'finally...' ... print 'END' ... try... ValueError: invalid literal for int() with base 10: 'a' finally... END
如果没有错误发生,可以在except
语句块后面加一个else
,当没有错误发生时,会自动执行else
语句:
>>> try: ... print 'try...' ... r = 10 / int('a') ... print 'result:' ,r ... except ValueError,e: ... print 'ValueError:',e ... except ZeroDivisionError,e: ... print 'ZeroDivisonError',e ... else: ... print 'no error!' ... finally: ... print 'finally...' ... print 'END'
所有的错误类型都继承自BaseException
,所以在使用except
时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。比如:
try:
foo()
except StandardError, e: #捕获当前种类异常,包括子类的.
print 'StandardError'
except ValueError, e: #捕获不到ValueError,因为被StandarError父类捕获了.
print 'ValueError'
异常类型继承关系参考:这里
不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了,因为使用try...except
捕获错误可以跨越多层调用:
def foo(s): return 10 / int(s) def bar(s): return foo(s) * 2 def main(): try: bar('0') except StandardError, e: print 'Error!' finally: print 'finally...'
如果错误没有被捕获,它就会一直往上抛,最后被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()出错了,在代码文件err.py的第11行代码 main() File "err.py", line 9, in main #调用bar('0')出错了,在代码文件err.py的第9行代码 bar('0') File "err.py", line 6, in bar #原因是return foo(s) * 2这个语句出错了,但这还不是最终原因 return foo(s) * 2 File "err.py", line 3, in foo #原因是return 10 / int(s)这个语句出错了,这是错误产生的源头 return 10 / int(s) ZeroDivisionError: integer division or modulo by zero
出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键
不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束,
既然能捕获错误,就把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去:
# err.py
import logging #内置的logging模块可以非常容易地记录错误信息,通过配置,logging
还可以把错误记录到日志文件里,方便事后排查
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except StandardError, e:
logging.exception(e)
main()
print 'END'
同样是出错,但程序打印完错误信息后会继续执行,并正常退出.
错误是class,捕获一个错误就是捕获到该class的一个实例.
因此,错误并不是凭空产生的,而是有意创建并抛出的.
如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后,用raise
语句抛出一个错误的实例:
# err.py class FooError(StandardError): pass def foo(s): n = int(s) if n==0: raise FooError('invalid value: %s' % s) return 10 / n
$ python err.py Traceback (most recent call last): ... __main__.FooError: invalid value: 0
只有在必要的时候才定义我们自己的错误类型,并且尽量使用Python内置的错误类型.
# err.py def foo(s): n = int(s) return 10 / n def bar(s): try: return foo(s) * 2 except StandardError, e: #捕获错误 print 'Error!' #只是记录一下,便于后续追踪 raise #继续往上抛,让顶层调用者处理 def main(): bar('0') main()
raise
语句如果不带参数,就会把当前错误原样抛出。except
中raise
一个Error,还可以把一种类型的错误转化成另一种类型:
try: 10 / 0 except ZeroDivisionError: raise ValueError('input error!') #可以合理的转换,但是决不应该把一个错误类型转换成毫不相干的.
程序也可以主动抛出错误,让调用者来处理相应的错误。但是,应该在文档中写清楚可能会抛出哪些错误,以及错误产生的原因.
1、print , 简单直接粗暴有效的用print把可能有问题的变量打印出来看看,执行后在输出中查找打印的变量值.
2、断言,assert: 凡是用print
来辅助查看的地方,都可以用断言(assert)来替代:
# err.py def foo(s): n = int(s) assert n != 0, 'n is zero!' #assert表达式n != 0应该是True,否则代码就会出错,断言失败,assert语句抛出AssertionError. return 10 / n def main(): foo('0')
启动Python解释器时可以用-O
参数来关闭assert,
然后
可以把所有的
assert
语句当成pass
来看。
$ python -O err.py Traceback (most recent call last): ... ZeroDivisionError: integer division or modulo by zero
3、logging: 和assert
比,logging
不会抛出错误,而且可以输出到文件:
#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
默认情况下,logging将日志打印到屏幕,日志级别为WARNING;
日志级别大小关系为:CRITICAL > ERROR > WARNING > INFO > DEBUG > NOTSET,当然也可以自己定义日志级别.
参考:这里
# err.py s = '0' n = int(s) print 10 / n
$ python -m pdb err.py > /Users/michael/Github/sicp/err.py(2)<module>() -> s = '0'
命令l
来查看代码:
命令n
可以单步执行代码:
命令p 变量名
来查看变量:
命令q
结束调试,退出程序:
也是用pdb,但不需要单步执行,只需要import pdb
,然后,在可能出错的地方放一个pdb.set_trace()
,就可以设置一个断点:
# err.py import pdb s = '0' n = int(s) pdb.set_trace() # 运行到这里会自动暂停 print 10 / n
程序会自动在pdb.set_trace()
暂停并进入pdb调试环境,可以用命令p
查看变量,或者用命令c
继续运行:
$ 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
1、Python IDE有PyCharm,设置断点、单步执行.
2、Eclipse加上pydev插件也可以调试Python程序。
三、单元测试 :是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
比如对函数abs()
,我们可以编写出以下几个测试用例:
1、输入正数,比如1
、1.2
、0.99
,期待返回值与输入相同;
2、输入负数,比如-1
、-1.2
、-0.99
,期待返回值与输入相反;
3、输入0
,期待返回0
;
4、输入非数值类型,比如None
、[]
、{}
,期待抛出TypeError
。
把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。
单元测试通过,说明测试的这个函数能够正常工作。单元测试不通过,要么函数有bug,要么测试条件输入不正确,需要修复函数使单元测试能够通过。
单元测试通过的意义,如果对abs()
函数代码做了修改,只需要再跑一遍单元测试。
如果通过,说明修改不会对abs()
函数原有的行为造成影响,
如果不通过,说明修改与原有行为不一致,要么修改代码,要么修改测试。
以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
编写一个Dict
类,使类的行为和dict
一致,但是可以通过属性来访问,用起来就像下面这样:
>>> d = Dict(a='q',b=Dict(b='c',k='D'))
>>> d.a
'q'
>>> d.b
{'k': 'D', 'b': 'c'}
#mydict.py class Dict(dict): def __init__(self, **kw): super(Dict, self).__init__(**kw) #super保证公共父类被执行一遍,参考这里 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
编写单元测试,需要引入Python自带的unittest
模块,编写mydict_test.py
如下:
#mydict_test.py
import unittest from mydict import Dict #导入我们之前创建的Dict类. class TestDict(unittest.TestCase): def test_init(self): #以test
开头的方法就是测试方法,不以test
不被认为是测试方法,测试的时候不会被执行 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): d = Dict() d['key'] = 'value' self.assertEquals(d.key, 'value') #断言d.key的值为'value' def test_attr(self): d = Dict() d.key = 'value' self.assertTrue('key' in d) #断言'key' in d 返回True.这里判断的是'key'键. self.assertEquals(d['key'], 'value') def test_keyerror(self): d = Dict() with self.assertRaises(KeyError): value = d['empty'] def test_attrerror(self): d = Dict() with self.assertRaises(AttributeError): #期待抛出指定类型的Error value = d.empty
最简单的运行方式是在编写好的单元测试模块最后加上两行代码:
if __name__ == '__main__': unittest.main()
这样就可以当做py脚本执行.
另一种更常见的方法是在命令行通过参数-m unittest
直接运行单元测试:
$ python -m unittest mydict_test ..... ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK
推荐使用参数-m unittest,因为这样可以一次批量运行很多单元测试,并且,有很多工具可以自动来运行这些单元测试。
在单元测试中编写两个特殊的setUp()
和tearDown()
方法。这两个方法会分别在每调用一个测试方法的前后分别被执行:
setUp()
和tearDown()
方法有什么用呢?设想一个测试需要启动一个数据库,这时,就可以在setUp()
方法中连接数据库,在tearDown()
方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码:
class TestDict(unittest.TestCase): def setUp(self): print 'setUp...' def tearDown(self): print 'tearDown...'
单元测试可以有效地测试某个程序模块的行为,是未来重构代码的信心保证。
单元测试的测试用例要覆盖常用的输入组合、边界条件和异常。
单元测试代码要非常简单,如果测试代码太复杂,那么测试代码本身就可能有bug。
单元测试通过了并不意味着程序就没有bug了,但是不通过程序肯定有bug。
四、文档测试:(doctest) 直接提取注释中的代码并执行测试.
一般的文档注释:
def abs(n): ''' Function to get absolute value of number. Example: >>> abs(1) 1 >>> abs(-1) 1 >>> abs(0) 0 ''' return n if n >= 0 else (-n)
Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用...
表示中间一大段烦人的输出。
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()
运行python mydict.py
:
$ python mydict.py
什么输出也没有。这说明编写的doctest运行都是正确的。如果程序有问题,比如把__getattr__()
方法注释掉,再运行就会报错:
$ python mydict.py ********************************************************************** File "mydict.py", line 7, in __main__.Dict Failed example: d1.x Exception raised: Traceback (most recent call last): ... AttributeError: 'Dict' object has no attribute 'x' ********************************************************************** File "mydict.py", line 13, in __main__.Dict Failed example: d2.c Exception raised: Traceback (most recent call last): ... AttributeError: 'Dict' object has no attribute 'c' **********************************************************************
注意到最后两行代码。当模块正常导入时,doctest不会被执行。只有在命令行运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行。
doctest非常有用,不但可以用来测试,还可以直接作为示例代码。通过某些文档生成工具,就可以自动把包含doctest的注释提取出来。用户看文档的时候,同时也看到了doctest。