文章目录
- 错误:
1、程序编写有问题造成的:这种错误我们通常称之为bug,bug是必须修复的。
2、用户输入造成的:这种错误可以通过检查用户输入来做相应的处理。
3、完全无法在程序运行过程中预测的:比如写入文件的时候,磁盘满了,写不进去了,或者从网络抓取数据,网络突然断掉了。这类错误也称为异常,在程序中通常是必须处理的,否则,程序会因为各种问题终止并退出。 - 解决办法
1、跟踪程序的执行,查看变量的值是否正确,这个过程称为调试。
2、Python的pdb可以让我们以单步方式执行代码。
1. 错误处理
1.1 错误代码
- 程序运行中,若发生错误,可以事先约定返回一个错误代码,例如:打开文件的函数
open()
,成功时返回文件描述符(一个整数),出错时返回-1
。 - 但是用这种的方式表示是否出错十分不便,因为函数本身应该返回的正常结果和错误代码混在一起:
def foo():
r = some_function()
if r==(-1):
return (-1)
# do something
return r
def bar():
r = foo()
if r==(-1):
print('Error')
else:
pass
1.2 try
- 按照上面错误代码的方式很容易出错,故大部分语言都内置了一套
try...except...finally...
的错误处理机制:
# 当我们认为某些代码可能会报错,就将这段代码放入try中来运行:
try:
print('try...')
r = 10 / 0
print('result:', r)
# 如果执行出错,后续代码不会运行,而是直接跳转至错误处理代码,即:except语句块:
except ZeroDivisionError as e:
print('except:', e)
# 执行完except语句后,如果有finally语句,则执行finally语句块,至此,执行完毕;
finally:
print('finally...')
print('END')
# 运行结果:
try...
except: division by zero
finally...
END
# 若将10 / 0 改为 10 / 2,则得:
try...
result: 5.0
finally...
END
# 可能同时有多种错误,那就需要由不同的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')
# 运行结果
try...
ValueError: invalid literal for int() with base 10: 'a'
finally...
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')
# python中错误类型其实也是calss,左右的错误类型都继承自BaseException,
# 所以在使用except时,它不但捕获该类型的错误,还将其子类也一网打尽,如:
try:
foo()
except ValueError as e:
print('ValueError')
except UnicodeError as e: # 此except永远捕获不到UnicodeError,因为UnicodeError是ValueError的子类
print('UnicodeError')
# Python所有的错误都是从BaseException类派生的:
# [常见错误类型和继承关系](https://docs.python.org/3/library/exceptions.html#exception-hierarchy)
# 跨越多层调用
# 函数main()调用bar(),bar()调用foo(),结果foo()出错,这时,只要main()捕获到,就可以处理,
# 也就是说只需要在合适的层次捕获错误即可,大大减少了写try...except...finally的麻烦:
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...')
1.3 调用栈
- 如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。来看看
err.py
:
# 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
-
出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链:
-
错误信息第1行:
Traceback (most recent call last):
- 告诉我们这是错误的跟踪信息,第2~3行:
File "err.py", line 11, in <module>
main()```
- 调用`main()`出错了,在代码文件`err.py`的第11行代码,但原因是第9行:
```python
File "err.py", line 9, in main
bar('0')
- 调用
bar('0')
出错了,在代码文件err.py
的第9行代码,但原因是第6行:
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
- 根据错误类型
ZeroDivisionError
,我们判断,int(s)
本身并没有出错,但是int(s)
返回0
,在计算10 / 0
时出错,至此,找到错误源头。
1.4 记录错误
- 既能捕获错误,同时将错误堆栈打印出来,让程序继续执行下去:
# Python内置的logging模块可以非常容易的记录错误信息:
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')
# 同样是出错,但程序打印完错误信息后会继续执行,并正常退出:
runfile('E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py', wdir='E:/4_Programe/1_Python/3_Code/1_LiaoDaDa')
ERROR:root:division by zero
Traceback (most recent call last):
File "E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py", line 11, in main
bar('0')
File "E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py", line 7, in bar
return foo(s) * 2
File "E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/15_logging.py", line 4, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END
- 通过配置,logging还可以把错误记录到日志文件里,方便事后排查。
1.5 抛出错误
- 错误是class,捕获一个错误就是捕获到该class的一个实例:
# 定义一个错误的class,选择好继承关系,用raise语句抛出一个错误的实例:
class FooError(ValueError):
pass
def foo(s):
n = int(s)
if n == 0:
raise FooError('invalid value: %s' % s)
return 10 / n
foo('0')
# 执行结果
$ python3 err_raise.py
Traceback (most recent call last):
File "err_throw.py", line 11, in <module>
foo('0')
File "err_throw.py", line 8, in foo
raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 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()
2. 调试
- 写程序时难免会出现bug,故需要一整套调试程序来修复bug:
2.1 print()
def foo(s):
n = int(s)
print('>>> n = %d' % n)
return 10 / n
def main():
foo('0')
main()
# 运行结果
>>> n = 0
Traceback (most recent call last):
...
ZeroDivisionError: integer division or modulo by zero
# 此方法会使程序变得十分臃肿,同时运行结果也会包含很多垃圾信息
2.2 断言
- 程序中凡是用
print()
来辅助查看的地方,都可使用断言(assert)来替代:
def foo(s):
n = int(s)
assert n != 0, 'n is zero!' # 此句意思为表达式 n != 0 应该是True,否则根据程序运行的逻辑,后面的代码肯定出错
return 10 / n
def main():
foo('0')
main()
# 运行结果
Traceback (most recent call last):
...
AssertionError: n is zero! # 如果断言失败,assert语句本身会抛出AssertionError
- 若程序中到处充斥着
assert
,和print()
相比好不到那里去,故python中允许使用-0
参数来关闭assert
:
$ python -O err.py
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
# 关闭后,你可以将所有的assert语句当做pass来看
2.3 logging
- 把
print()
替换为logging
,和assert
相比,logging
不会抛出错误,而且可以输出到文件: logging
可以指定记录信息的级别,有debug
,info
,warning
,error
几个级别,从低到高,当我们指定level=INFO
时,logging.debug
就不起作用了,这样,我们就可以放心的输出不同级别的信息,也不用删除,最后统一控制输出那个级别的信息:
import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info('n = %d' % n) # 可以输出一段文本
print(10 / n)
# 运行结果
INFO:root:n = 0
Traceback (most recent call last):
File "err.py", line 8, in <module>
print(10 / n)
ZeroDivisionError: division by zero
2.4 pdb
- 第四种方式是启动python的调试器pdb,让程序以单步方式运行:
# err.py
s = '0'
n = int(s)
print(10 / n)
- 然后启动:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python -m pdb err.py
> e:\4_programe\1_python\3_code\1_liaodada\err.py(38)<module>()
-> s = '0'
- 以参数
-m pdb
启动后,pdb定位到下一步要执行的代码-> s = '0'
。输入命令l
来查看代码:
(Pdb) l
1 # test.py
2 -> s = '0'
3 n = int(s)
4 print(10 / n)
- 输入命令
n
可以单步执行代码:
(Pdb) n
> e:\4_programe\1_python\3_code\1_liaodada\test.py(3)<module>()
-> n = int(s)
(Pdb) n
> e:\4_programe\1_python\3_code\1_liaodada\test.py(4)<module>()
-> print(10 / n)
- 任何时候都可以输入命令
p 变量名
来查看变量:
(Pdb) p s
'0'
(Pdb) p n
0
- 输入命令
q
结束调试,退出程序
2.5 pdb.set_trace()
- 此方法也是pdb,不过不需要单步执行,只需在可能出错的地方放一个
pdb.set_trace()
,就可以设置一个断点:
import pdb
s = '0'
n = int(s)
pdb.set_trace() # 运行到此会自动暂停
print(10 / n)
# 运行代码,程序会在pdb.set_trace()暂停并进入pdb调试环境:
runfile('E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/18_err.py', wdir='E:/4_Programe/1_Python/3_Code/1_LiaoDaDa')
> e:\4_programe\1_python\3_code\1_liaodada\18_err.py(51)<module>()
-> print(10 / n)
(Pdb) >? p n
0
(Pdb) >? c
Traceback (most recent call last):
File "18_err.py", line 51, in <module>
print(10 / n)
ZeroDivisionError: division by zero
# 此方式虽然比单步pdb效率高,但也不是最佳选择
2.6 IDE
3. 单元测试
- 单元测试是用来对一个模块,一个函数或者一个类来进行正确性检测的测试工作:
- 比如对函数
abs()
,我们可以编写出以下几个测试用例:-
输入正数,比如
1
、1.2
、0.99
,期待返回值与输入相同; -
输入负数,比如
-1
、-1.2
、-0.99
,期待返回值与输入相反; -
输入
0
,期待返回0
; -
输入非数值类型,比如
None
、[]
、{}
,期待抛出TypeError
。
-
- 可以把上面的测试用例放到一个测试模块里,就是一个完整的单元测试;
3.1 编写单元测试
# 编写一个Dict类,类的行为和dict一致,可以通过属性来访问:
>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1
# 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
import unittest
from mydict import Dict
class TestDict(unittest.TestCase): # 编写一个测试类,从unittest.TestCase继承
# 以test开头的方法就是测试方法,否则不是,在测试的时候不会被执行
# 对每一类测试都需要编写一个test.xxx()方法,unittest.TestCase提供了很多内置的条件判断
# 只需要调用这些方法就可以断言输出是否是我们所期望的,最常用的断言是assertEqual()
def test_init(self):
d = Dict(a = 1, b = 'test')
self.assertEqual(d.a, 1) # self.assertEqual(abs(-1), 1) # 断言函数返回的结果与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): # 此断言是期待抛出指定类型的Error
value = d['empty'] # 通过d['empty']访问不存在的key时,断言会抛出KeyError
def test_attrerror(self):
d = Dict()
with self.assertRaises(AttributeError):
value = d.empty # 通过d.empty访问不存在的key时,抛出AttributError
3.2 运行单元测试
- 在
19_mydict_test.py
的最后加上:
if __name__ = '__main__'
unittest.main()
- 运行
# 把 19_mydict_test.py 当做正常的python脚本运行:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python 20_mydict_test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK
# 或者在命令行通过参数 -m unittest 直接运行单元测试:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python -m unittest 19_mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK
3.3 setUp
与tearDown
- 可以在单元测试中编写两个特殊的
setUp()
和tearDown()
方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
class TestDict(unittest.TestCase):
def setUp(self):
print('setUp...')
def tearDown(self):
print('tearDown...')
- 运行结果:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa>python -m unittest 19_mydict_test
setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.setUp...
tearDown...
.
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK
3.4 EX
- 对Student类编写单元测试,结果发现测试不通过,请修改Student类,让测试通过:
# -*- coding: utf-8 -*-
import unittest
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
if self.score >= 60:
return 'B'
if self.score >= 80:
return 'A'
return 'C'
- 进行如下修改:
# -*- coding: utf-8 -*-
import unittest
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
if 80 <= self.score <= 100:
return 'A'
elif 60 <= self.score <= 80:
return 'B'
elif 0 <= self.score < 60:
return 'C'
else:
raise ValueError
class TestStudent(unittest.TestCase):
def test_80_to_100(self):
s1 = Student('Bart', 80)
s2 = Student('Lisa', 100)
self.assertEqual(s1.get_grade(), 'A')
self.assertEqual(s2.get_grade(), 'A')
def test_60_to_80(self):
s1 = Student('Bart', 60)
s2 = Student('Lisa', 79)
self.assertEqual(s1.get_grade(), 'B')
self.assertEqual(s2.get_grade(), 'B')
def test_0_to_60(self):
s1 = Student('Bart', 0)
s2 = Student('Lisa', 59)
self.assertEqual(s1.get_grade(), 'C')
self.assertEqual(s2.get_grade(), 'C')
def test_invalid(self):
s1 = Student('Bart', -1)
s2 = Student('Lisa', 101)
with self.assertRaises(ValueError):
s1.get_grade()
with self.assertRaises(ValueError):
s2.get_grade()
if __name__ == '__main__':
unittest.main()
- 运行结果:
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
4. 文档测试
4.1 doctest
- python官方文档中都有很多示例代码,这些代码和其他说明可以写在注释中,并且可以直接粘贴出来运行,那可不可以自动执行写在注释中的这些代码呢?
- 使用
doctest
模块可以直接提取注释中的代码并执行测试:
# 使用doctest来测试上次编写的Dict类
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 20_doctest.py
,发现什么输出都没有,这说明我们编写的doctest运行是正确的。如果把__getattr__
方法注释掉,再次运行:
E:\4_Programe\1_Python\3_Code\1_LiaoDaDa> python 20_doctest.py
**********************************************************************
File "20_doctest.py", line 7, in __main__.Dict
Failed example:
d1.x
Exception raised:
Traceback (most recent call last):
File "E:\1_Install_Total\8_Anaconda\lib\doctest.py", line 1329, in __run
compileflags, 1), test.globs)
File "<doctest __main__.Dict[2]>", line 1, in <module>
d1.x
AttributeError: 'Dict' object has no attribute 'x'
**********************************************************************
File "20_doctest.py", line 13, in __main__.Dict
Failed example:
d2.c
Exception raised:
Traceback (most recent call last):
File "E:\1_Install_Total\8_Anaconda\lib\doctest.py", line 1329, in __run
compileflags, 1), test.globs)
File "<doctest __main__.Dict[6]>", line 1, in <module>
d2.c
AttributeError: 'Dict' object has no attribute 'c'
**********************************************************************
1 items had failures:
2 of 9 in __main__.Dict
***Test Failed*** 2 failures.
- 注意到最后3行代码。当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行。
4.2 EX
- 对函数
fact(n)
编写doctest并执行:
def fact(n):
'''
Calculate 1*2*...*n
>>> fact(1)
1
>>> fact(10)
?
>>> fact(-1)
?
'''
if n < 1:
raise ValueError()
if n == 1:
return 1
return n * fact(n - 1)
if __name__ == '__main__':
import doctest
doctest.testmod()
- 修改之后:
def fact(n):
'''
Calculate 1*2*...*n
>>> fact(1)
1
>>> fact(10)
3628800
>>> fact(-1)
Traceback (most recent call last):
...
ValueError: n must be >= 1
'''
if n < 1:
raise ValueError()
if n == 1:
return 1
return n * fact(n - 1)
if __name__ == '__main__':
import doctest
doctest.testmod()
# 运行结果
**********************************************************************
File "C:\Users\Jack_ZD\AppData\Local\Temp\learn_python_zaw1p1zl_py\test_1.py", line 10, in __main__.fact
Failed example:
fact(-1)
Expected:
Traceback (most recent call last):
...
ValueError: n must be >= 1
Got:
Traceback (most recent call last):
File "E:\1_Install_Total\8_Anaconda\lib\doctest.py", line 1329, in __run
compileflags, 1), test.globs)
File "<doctest __main__.fact[2]>", line 1, in <module>
fact(-1)
File "C:\Users\Jack_ZD\AppData\Local\Temp\learn_python_zaw1p1zl_py\test_1.py", line 16, in fact
raise ValueError()
ValueError
**********************************************************************
1 items had failures:
1 of 3 in __main__.fact
***Test Failed*** 1 failures.