错误处理

try…except…finally…

  1. try:
  2. print('try')
  3. r = 10 / 0
  4. print('result:', r)
  5. except ZeroDivisionError as e:
  6. print('except:', e)
  7. finally:
  8. print('finally...')
  9. print('END')
  10. # try...
  11. # except: division by zero
  12. # finally...
  13. # END

如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码except语句块。执行完except后,如果有finally语句块,则执行finally语句块。

可以有多个**except**来捕获不同类型的错误:

  1. try:
  2. print('try...')
  3. r = 10 / int('a')
  4. print('result:', r)
  5. except ValueError as e:
  6. print('ValueError:', e)
  7. except ZeroDivisionError as e:
  8. print('ZeroDivisionError:', e)
  9. finally:
  10. print('finally...')
  11. print('END')

如果没有错误发生,可以在**except**语句块后面加一个**else**,当没有错误发生时,会自动执行**else**语句:

  1. try:
  2. print('try...')
  3. r = 10 / int('2')
  4. print('result:', r)
  5. except ValueError as e:
  6. print('ValueError:', e)
  7. except ZeroDivisionError as e:
  8. print('ZeroDivisionError:', e)
  9. else:
  10. print('no error!')
  11. finally:
  12. print('finally...')
  13. print('END')

Python的错误其实也是class, 所有的错误类型都继承自BaseExcepetion,所以在使用except时,需要注意它不但捕获该类型的错误,还把其子类也“一网打尽”。

  1. try:
  2. foo()
  3. except ValueError as e:
  4. print('ValueError')
  5. except UnicodeError as e:
  6. print('UnicodeError')

第二个except永远也捕获不到UnicodeError,因为UnicodeErrorValueError的子类。如果有,也被第一个except给捕获了。

Python常见的错误类型和继承关系

  1. BaseException
  2. +-- SystemExit
  3. +-- KeyboardInterrupt
  4. +-- GeneratorExit
  5. +-- Exception
  6. +-- StopIteration
  7. +-- StopAsyncIteration
  8. +-- ArithmeticError
  9. | +-- FloatingPointError
  10. | +-- OverflowError
  11. | +-- ZeroDivisionError
  12. +-- AssertionError
  13. +-- AttributeError
  14. +-- BufferError
  15. +-- EOFError
  16. +-- ImportError
  17. | +-- ModuleNotFoundError
  18. +-- LookupError
  19. | +-- IndexError
  20. | +-- KeyError
  21. +-- MemoryError
  22. +-- NameError
  23. | +-- UnboundLocalError
  24. +-- OSError
  25. | +-- BlockingIOError
  26. | +-- ChildProcessError
  27. | +-- ConnectionError
  28. | | +-- BrokenPipeError
  29. | | +-- ConnectionAbortedError
  30. | | +-- ConnectionRefusedError
  31. | | +-- ConnectionResetError
  32. | +-- FileExistsError
  33. | +-- FileNotFoundError
  34. | +-- InterruptedError
  35. | +-- IsADirectoryError
  36. | +-- NotADirectoryError
  37. | +-- PermissionError
  38. | +-- ProcessLookupError
  39. | +-- TimeoutError
  40. +-- ReferenceError
  41. +-- RuntimeError
  42. | +-- NotImplementedError
  43. | +-- RecursionError
  44. +-- SyntaxError
  45. | +-- IndentationError
  46. | +-- TabError
  47. +-- SystemError
  48. +-- TypeError
  49. +-- ValueError
  50. | +-- UnicodeError
  51. | +-- UnicodeDecodeError
  52. | +-- UnicodeEncodeError
  53. | +-- UnicodeTranslateError
  54. +-- Warning
  55. +-- DeprecationWarning
  56. +-- PendingDeprecationWarning
  57. +-- RuntimeWarning
  58. +-- SyntaxWarning
  59. +-- UserWarning
  60. +-- FutureWarning
  61. +-- ImportWarning
  62. +-- UnicodeWarning
  63. +-- BytesWarning
  64. +-- ResourceWarning

使用try...except捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()调用bar()bar()调用foo(),结果foo()出错了,这时,只要main()捕获到了,就可以处理:

  1. def foo(s):
  2. return 10 / int(s)
  3. def bar(s):
  4. return foo(s) * 2
  5. def main():
  6. try:
  7. bar('0')
  8. except Exception as e:
  9. print('Error:', e)

调用栈

如果错误没有被捕获,他就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出,来看看err.py

  1. #err.py
  2. def foo(s):
  3. return 10 / int(s)
  4. def bar(s):
  5. return foo(s) * 2
  6. def main():
  7. bar('0')
  8. main()

执行结果如下:

  1. $ python3 err.py
  2. Traceback (most recent call last): # 错误的跟踪信息
  3. File "err.py", line 11, in <module>
  4. main() # 调用main()出错了,在代码文件err.py的第11行代码
  5. File "err.py", line 9, in main # 但原因是第9行
  6. bar('0') # 调用bar('0')出错了,在代码文件err.py的第9行代码
  7. File "err.py", line 6, in bar # 但原因是第6行
  8. return foo(s) * 2 # 原因是return foo(s) * 2这个语句出错了,但这还不是最终原因
  9. File "err.py", line 3, in foo
  10. return 10 / int(s) # 原因是return 10 / int(s)这个语句出错了,这是错误产生的源头
  11. ZeroDivisionError: division by zero
  12. # 根据错误类型ZeroDivisionError,我们判断,int(s)本身并没有出错,
  13. # 但是int(s)返回0,在计算10 / 0时出错,至此,找到错误源头。

记录错误:logging

如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。

Python内置的logging模块可以非常容易地记录错误信息:

  1. # err_logging.py
  2. import logging
  3. def foo(s):
  4. return 10 / int(s)
  5. def bar(s):
  6. return foo(s) * 2
  7. def main():
  8. try:
  9. bar('0')
  10. except Exception as e:
  11. logging.exception(e)
  12. main()
  13. print('END')

同样是出错,但程序打印完错误信息后会继续执行,并正常退出:

  1. $ python3 err_logging.py
  2. ERROR:root:division by zero
  3. Traceback (most recent call last):
  4. File "err_logging.py", line 13, in main
  5. bar('0')
  6. File "err_logging.py", line 9, in bar
  7. return foo(s) * 2
  8. File "err_logging.py", line 6, in foo
  9. return 10 / int(s)
  10. ZeroDivisionError: division by zero
  11. END

通过配置,logging还可以把错误记录到日志文件里,方便事后排查。

抛出错误:raise

因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。

如果要抛出错误,首先根据需要,可以定义一个错误的class,选择好继承关系,然后用raise抛出一个错误的实例:

  1. # err_raise.py
  2. class FooError(ValueError):
  3. pass
  4. def foo(s):
  5. n = int(s)
  6. if n==0:
  7. raise FooError('invalid value: %s' % s)
  8. return 10 / n
  9. foo('0')

执行可以最后跟踪到我们定义的错误:

  1. $ python3 err_raise.py
  2. Traceback (most recent call last):
  3. File "err_throw.py", line 11, in <module>
  4. foo('0')
  5. File "err_throw.py", line 8, in foo
  6. raise FooError('invalid value: %s' % s)
  7. __main__.FooError: invalid value: 0

只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型(比如ValueErrorTypeError),尽量使用Python内置的错误类型。

最后,我们来看另一种错误处理的方式:

  1. # err_reraise.py
  2. def foo(s):
  3. n = int(s)
  4. if n==0:
  5. raise ValueError('invalid value: %s' % s)
  6. return 10 / n
  7. def bar():
  8. try:
  9. foo('0')
  10. except ValueError as e:
  11. print('ValueError!')
  12. raise # 已经捕获了错误,但是,打印一个ValueError!后,又把错误通过raise语句抛出去了
  13. bar()

由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。
**raise**语句如果不带参数,就会把当前错误原样抛出。

调试

使用print()把可能有问题的变量打印出来,简单直接粗暴:

  1. def foo(s):
  2. n = int(s)
  3. print('>>> n = %d' % n)
  4. return 10 / n
  5. def main():
  6. foo('0')
  7. main()

执行后在输出中查找打印的变量值:

  1. $ python err.py
  2. >>> n = 0
  3. Traceback (most recent call last):
  4. ...
  5. ZeroDivisionError: integer division or modulo by zero

print()最大的坏处是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息。所以,我们又有第二种方法。

断言

凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:

  1. def foo(s):
  2. n = int(s)
  3. assert n != 0, 'n is zero'
  4. return 10 / n
  5. def main():
  6. foo('0')

assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。

如果断言失败,assert语句本身就会抛出AssertionError

  1. $ python err.py
  2. Traceback (most recent call last):
  3. ...
  4. AssertionError: n is zero!

启动Python解释器时可以用-O参数来关闭assert

  1. $ python -O err.py
  2. Traceback (most recent call last):
  3. ...
  4. ZeroDivisionError: division by zero

关闭后,你可以把所有的assert语句当成pass来看。

logging

print()替换成logging是第三种方式,和asserts比,logging不会抛出错误,而且可以输出到文件:

  1. import logging
  2. logging.basicConfig(level=logging.INFO)
  3. s = '0'
  4. n = int(s)
  5. logging.info('n = %d' % n)
  6. print(10 / n)

输出:

  1. $ python err.py
  2. INFO:root:n = 0
  3. Traceback (most recent call last):
  4. File "err.py", line 8, in <module>
  5. print(10 / n)
  6. ZeroDivisionError: division by zero

这就是logging的好处,它允许你指定记录信息的级别,有debuginfowarningerror等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debuginfo就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。

pdb

第4种方式是启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。

  1. # err.py
  2. s = '0'
  3. n = int(S)
  4. print(10 / n)

然后启动:

  1. $ python -m pdb err.py
  2. > /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()
  3. -> s = '0'

以参数-m pdb启动后,pdb定位到下一步要执行的代码-> s = '0'。输入命令l(字母l不是数字1)来查看代码:

  1. (Pdb) l
  2. 1 # err.py
  3. 2 -> s = '0'
  4. 3 n = int(s)
  5. 4 print(10 / n)

输入命令n可以单步执行代码:

  1. (Pdb) n
  2. > /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()
  3. -> n = int(s)
  4. (Pdb) n
  5. > /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>()
  6. -> print(10 / n)

任何时候都可以输入命令p 变量名来查看变量:

  1. (Pdb) p s
  2. '0'
  3. (Pdb) p n
  4. 0

输入命令q结束调试,退出程序:

  1. (Pdb) q

pdb.set_trace()

这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb,然后,在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点:

  1. # err.py
  2. import pdb
  3. s = '0'
  4. n = int(s)
  5. pdb.set_trace() # 运行到这里会自动暂停
  6. print(10 / n)

运行代码,程序会自动在pdb.set_trace()暂停并进入pdb调试环境,可以用命令p查看变量,或者用命令c继续运行:

  1. $ python err.py
  2. > /Users/michael/Github/learn-python3/samples/debug/err.py(7)<module>()
  3. -> print(10 / n)
  4. (Pdb) p n
  5. 0
  6. (Pdb) c
  7. Traceback (most recent call last):
  8. File "err.py", line 7, in <module>
  9. print(10 / n)
  10. ZeroDivisionError: division by zero

这个方式比直接启动pdb单步调试效率要高很多,但也高不到哪去。

IDE

如果要比较爽地设置断点、单步执行,就需要一个支持调试功能的IDE。目前比较好的Python IDE有:
Visual Studio Code:https://code.visualstudio.com/,需要安装Python插件。
PyCharm:http://www.jetbrains.com/pycharm/

单元测试

我们来编写一个Dict类,这个类的行为和dict一致,但是可以通过属性来访问,用起来就像下面这样:

  1. >>> d = Dict(a=1, b=2)
  2. >>> d['a']
  3. 1
  4. >>> d.a
  5. 1
  1. # mydict.py
  2. class Dict(dict):
  3. def __init__(self, **kw):
  4. super().__init__(**kw)
  5. def __getattr__(self, key):
  6. try:
  7. return self[key]
  8. except KeyError:
  9. raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
  10. def __setattr__(self, key, value):
  11. self[key] = value

为了编写单元测试,我们需要引入Python自带的unittest模块,编写mydict_test.py如下:

  1. # mydict_test.py
  2. import unittest
  3. from mydict import Dict
  4. class TestDict(unittest.TestCase):
  5. # 以test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。
  6. def test_init(self):
  7. d = Dict(a=1, b='test')
  8. self.assertEqual(d.a, 1) # 断言d.a返回的结果与1相等
  9. self.assertEqual(d.b, 'test')
  10. self.assertTrue(isinstance(d, dict))
  11. def test_key(self):
  12. d = Dict()
  13. d['key'] = 'value'
  14. self.assertEqual(d.key, 'value')
  15. def test_attr(self):
  16. d = Dict()
  17. d.key = 'value'
  18. self.assertTrue('key' in d)
  19. self.assertEqual(d['key'], 'value')
  20. def test_keyerror(self):
  21. d = Dict()
  22. with self.assertRaises(KeyError): # 期待抛出指定类型的Error
  23. value = d['empty'] # 通过d['empty']访问不存在的key时,断言会抛出KeyError:
  24. def test_attrerror(self):
  25. d = Dict()
  26. with self.assertRaises(AttributeError):
  27. value = d.empty # 通过d.empty访问不存在的key时,我们期待抛出AttributeError

运行单元测试

最简单的运行方式是在mydict_test.py的最后加上两行代码:

  1. if __name__ == '__main__'
  2. unittest.main()

这样就可以把mydict_test.py当做正常的python脚本运行:

  1. $ python mydict_test.py

另一种方法是在命令行通过参数-m unittest直接运行单元测试(推荐):

  1. $ python -m unittest mydict_test
  2. .....
  3. ----------------------------------------------------------------------
  4. Ran 5 tests in 0.000s
  5. OK

setUp与tearDown

可以在单元测试中编写两个特殊的setUp()tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。
设想你的测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库:

  1. class TestDict(unittest.TestCase):
  2. def setUp(self):
  3. print('setUp...')
  4. def tearDown(self):
  5. print('tearDown...')

文档测试

当我们编写注释时,如果写上这样的注释,无疑更明确地告诉函数的调用者该函数的期望输入和输出。:

  1. def abs(n):
  2. '''
  3. Function to get absolute value of number.
  4. Example:
  5. >>> abs(1)
  6. 1
  7. >>> abs(-1)
  8. 1
  9. >>> abs(0)
  10. 0
  11. '''
  12. return n if n >= 0 else (-n)

并且,Python内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。
doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用...表示中间一大段烦人的输出。

让我们用doctest来测试上次编写的Dict类:

  1. # mydict2.py
  2. class Dict(dict):
  3. '''
  4. Simple dict but also support access as x.y style.
  5. >>> d1 = Dict()
  6. >>> d1['x'] = 100
  7. >>> d1.x
  8. 100
  9. >>> d1.y = 200
  10. >>> d1['y']
  11. 200
  12. >>> d2 = Dict(a=1, b=2, c='3')
  13. >>> d2.c
  14. '3'
  15. >>> d2['empty']
  16. Traceback (most recent call last):
  17. ...
  18. KeyError: 'empty'
  19. >>> d2.empty
  20. Traceback (most recent call last):
  21. ...
  22. AttributeError: 'Dict' object has no attribute 'empty'
  23. '''
  24. def __init__(self, **kw):
  25. super(Dict, self).__init__(**kw)
  26. def __getattr__(self, key):
  27. try:
  28. return self[key]
  29. except KeyError:
  30. raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
  31. def __setattr__(self, key, value):
  32. self[key] = value
  33. if __name__=='__main__': # 当模块正常导入时,doctest不会被执行
  34. import doctest # 只有在命令行直接运行时,才执行doctest
  35. doctest.testmod()

运行python mydict2.py

  1. $ python mydict2.py

什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题,比如把__getattr__()方法注释掉,再运行就会报错:

  1. $ python mydict2.py
  2. **********************************************************************
  3. File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 10, in __main__.Dict
  4. Failed example:
  5. d1.x
  6. Exception raised:
  7. Traceback (most recent call last):
  8. ...
  9. AttributeError: 'Dict' object has no attribute 'x'
  10. **********************************************************************
  11. File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 16, in __main__.Dict
  12. Failed example:
  13. d2.c
  14. Exception raised:
  15. Traceback (most recent call last):
  16. ...
  17. AttributeError: 'Dict' object has no attribute 'c'
  18. **********************************************************************
  19. 1 items had failures:
  20. 2 of 9 in __main__.Dict
  21. ***Test Failed*** 2 failures.