09 Python 错误调试测试
By Kevin Song
- 09-01 错误处理
- 09-02 调试
- 09-03 单元测试
- 09-04 文档测试
09-01 错误处理
try…except…finally…
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
finally:
print('finally...')
print('END')
try...
except: division by zero
finally...
END
- 执行try内语句,如果出错,后续代码不会继续执行,直接跳转至except
- 执行except
- 执行finally
多种类型的错误
多个except捕获不同类型的错误
try:
print('try...')
r = 10 / int('a')
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
finally:
print('finally...')
print('END')
如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句
try:
print('try...')
r = 10 / int('2')
print('result:', r)
except ValueError as e:
print('ValueError:', e)
except ZeroDivisionError as e:
print('ZeroDivisionError:', e)
else:
print('no error!')
finally:
print('finally...')
print('END')
所有的错误类型都继承自 BaseException:https://docs.python.org/3/library/exceptions.html#exception-hierarchy
下面代码第二个except永远也捕获不到UnicodeError,因为UnicodeError是ValueError的子类,如果有,也被第一个except给捕获了
try:
foo()
except ValueError as e:
print('ValueError')
except UnicodeError as e:
print('UnicodeError')
优点:不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
print('Error:', e)
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()
$ python3 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: division by zero
记录错误
Python内置的logging模块可以非常容易地记录错误信息
# err_logging.py
import logging
def foo(s):
return 10 / int(s)
def bar(s):
return foo(s) * 2
def main():
try:
bar('0')
except Exception as e:
logging.exception(e)
main()
print('END')
同样是出错,但程序打印完错误信息后会继续执行,并正常退出:
$ python3 err_logging.py
ERROR:root:division by zero
Traceback (most recent call last):
File "err_logging.py", line 13, in main
bar('0')
File "err_logging.py", line 9, in bar
return foo(s) * 2
File "err_logging.py", line 6, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END
抛出错误
- 定义错误的class
- 选择继承关系
- raise抛出一个错误的实例
# err_raise.py
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n==0:
raise FooError('invalid value: %s' % s)
return 10 / n
foo('0')
一层一层抛出
# err_reraise.py
def foo(s):
n = int(s)
if n==0:
raise ValueError('invalid value: %s' % s)
return 10 / n
def bar():
try:
foo('0')
except ValueError as e:
print('ValueError!')
raise
bar()
注意: raise语句如果不带参数,就会把当前错误原样抛出
在except中raise一个Error,可以把一种类型的错误转化成另一种类型
try:
10 / 0
except ZeroDivisionError:
raise ValueError('input error!')
注意:转换必须合理,不应该把一个IOError转换成毫不相干的ValueError
09-02 调试
调试方法一:直接打印
def foo(s):
n = int(s)
print('>>> n = %d' % n)
return 10 / n
def main():
foo('0')
main()
$ python3 err.py
>>> n = 0
Traceback (most recent call last):
...
ZeroDivisionError: integer division or modulo by zero
缺点:调试完毕需要手动删除print
调试方法二:断言(assert)
格式:assert 表达式1, 表达式2
- 表达式1:返回布尔型
- 表达式2:断言失败时输出的失败消息的字符串
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n
def main():
foo('0')
断言失败
$ python3 err.py
Traceback (most recent call last):
...
AssertionError: n is zero!
启动Python解释器时可以用-O参数来关闭assert,关闭后,所有的assert相当于pass
调试方法三:logging
和assert比,logging不会抛出错误,而且可以输出到文件
import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)
记录信息的级别
- debug
- info:level=logging.INFO
- warning
- error
调试方法三:pdb
作用:让程序以单步方式运行,可以随时查看运行状态
# err.py
s = '0'
n = int(s)
print(10 / n)
启动pdb
$ python3 -m pdb err.py
> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()
-> s = '0'
输入命令 l 查看代码
(Pdb) l
1 # err.py
2 -> s = '0'
3 n = int(s)
4 print(10 / n)
输入命令 n 单步执行代码
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()
-> n = int(s)
(Pdb) n
> /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>()
-> print(10 / n)
输入命令 p 变量名 查看变量
(Pdb) p s
'0'
(Pdb) p n
0
输入命令 q 结束调试
(Pdb) q
调试方法四:pdb.set_trace()
不需要单步执行的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继续运行
$ python3 err.py
> /Users/michael/Github/learn-python3/samples/debug/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: division by zero
09-03 单元测试
类似于Codewars的attempt
定义:用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作
编写Dict类:mydict.py
class Dict(dict):
def __init__(self, **kw):
super().__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
编写mydict_test.py
import unittest
from mydict import Dict
class TestDict(unittest.TestCase):
def test_init(self):
d = Dict(a=1, b='test')
self.assertEqual(d.a, 1)
self.assertEqual(d.b, 'test')
self.assertTrue(isinstance(d, dict))
def test_key(self):
d = Dict()
d['key'] = 'value'
self.assertEqual(d.key, 'value')
def test_attr(self):
d = Dict()
d.key = 'value'
self.assertTrue('key' in d)
self.assertEqual(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):
value = d.empty
每一类测试都需要编写一个test_xxx()方法,unittest.TestCase提供了很多内置的条件判断,调用这些方法就可以断言输出结果
相等断言:assertEqual()
self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等
抛出指定类型的Error断言:assertRaises()
通过d[‘empty’]访问不存在的key时,断言会抛出KeyError
with self.assertRaises(KeyError):
value = d['empty']
通过d.empty访问不存在的key时,期待抛出AttributeError
with self.assertRaises(AttributeError):
value = d.empty
运行单元测试
方法一: 在mydict_test.py的最后加上两行代码:
if __name__ == '__main__':
unittest.main()
$ python3 mydict_test.py
方法二: 在命令行通过参数-m unittest直接运行单元测试
$ python3 -m unittest mydict_test
.....
-----------------------------------------------------------------
Ran 5 tests in 0.000s
OK
setUp与tearDown
setUp与tearDown这两个方法会分别在每调用一个测试方法的前后分别被执行
示例:测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码:
class TestDict(unittest.TestCase):
def setUp(self):
print('setUp...')
def tearDown(self):
print('tearDown...')
09-04 文档测试
自动执行写在注释中的代码
# mydict2.py
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()
运行python3 mydict2.py:
$ python3 mydict2.py
什么输出也没有。说明编写的doctest运行都是正确的。如果程序有问题,比如把getattr()方法注释掉,再运行就会报错:
$ python3 mydict2.py
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 10, in __main__.Dict
Failed example:
d1.x
Exception raised:
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'x'
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 16, in __main__.Dict
Failed example:
d2.c
Exception raised:
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'c'
**********************************************************************
1 items had failures:
2 of 9 in __main__.Dict
***Test Failed*** 2 failures.
当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行