python3 基础 廖雪峰教程笔记-5 异常错误调试

https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/00143191375461417a222c54b7e4d65b258f491c093a515000
错误、调试、测试

1.错误处理
高级语言通常都内置了一套try...except...finally...的错误处理机制,Python也不例外。

try 机制
try:
print('try...')
r = 10 / 0
print('result:', r)
except ZeroDivisionError as e:
print('except:', e)
except Exception as ex:
print("Exception:",ex)
else:
print("no error")

finally:
print('finally...')
print('END')

当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,
而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,
至此,执行完毕。

如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句
如果有多个异常,那么异常就会按顺序捕获下来,如果发生的异常没有,那么程序就会报错,所
以一般捕获异常,最后一个异常是范围最广的那个
异常关系
BaseException
+-- SystemExit (系统退出)
+-- KeyboardInterrupt (键盘中断)
+-- GeneratorExit (生产者退出)
+-- Exception (异常)
+-- StopIteration (停止迭代)
+-- StopAsyncIteration (通知异步迭代)
+-- ArithmeticError (算术错误)
| +-- FloatingPointError(浮点错误)
| +-- OverflowError (溢出错误)
| +-- ZeroDivisionError (零分的错误)
+-- AssertionError (断言错误)
+-- AttributeError (属性错误)
+-- BufferError (缓冲区的错误)
+-- EOFError (EOF错误)
+-- ImportError (导入错误)
+-- ModuleNotFoundError(模块未找到错误)
+-- LookupError (查找错误)
| +-- IndexError (索引错误)
| +-- KeyError (键值key错误)
+-- MemoryError (内存错误)
+-- NameError (名称错误)
| +-- UnboundLocalError (未绑定本地的错误)
+-- OSError (系统错误)
| +-- BlockingIOError (块IO错误)
| +-- ChildProcessError (子进程错误)
| +-- ConnectionError (连接错误)
| | +-- BrokenPipeError (管道断开错误)
| | +-- ConnectionAbortedError (连接失败错误)
| | +-- ConnectionRefusedError (拒绝连接错误)
| | +-- ConnectionResetError (连接重置错误)
| +-- FileExistsError (文件存在的错误)
| +-- FileNotFoundError (文件不存在的错误)
| +-- InterruptedError (中断的错误)
| +-- IsADirectoryError (是一个文件目录的错误)
| +-- NotADirectoryError(不是一个文件目录的错误)
| +-- PermissionError (权限错误)
| +-- ProcessLookupError(进程查看错误)
| +-- TimeoutError (超时错误)
+-- ReferenceError (引用错误)
+-- RuntimeError (运行时错误)
| +-- NotImplementedError(没有实现的错误)
| +-- RecursionError (递归错误)
+-- SyntaxError (语法错误)
| +-- IndentationError (缩进错误)
| +-- TabError (标签错误)
+-- SystemError (系统错误)
+-- TypeError (类型错误)
+-- ValueError (值错误)
| +-- UnicodeError (统一的字符编码标准 错误)
| +-- UnicodeDecodeError (编码解码错误)
| +-- UnicodeEncodeError (编码编译错误)
| +-- UnicodeTranslateError(编码转换错误)
+-- Warning (警告)
+-- DeprecationWarning (弃用警告)
+-- PendingDeprecationWarning (等待弃用警告)
+-- RuntimeWarning (运行时警告)
+-- SyntaxWarning (语法警告)
+-- UserWarning (用户警告)
+-- FutureWarning (未来警告)
+-- ImportWarning (导入警告)
+-- UnicodeWarning (编码警告)
+-- BytesWarning (字节警告)
+-- ResourceWarning (资源警告)

使用try...except捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()调用foo(),foo()调用bar(),
结果bar()出错了,这时,只要main()捕获到了,就可以处理:
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...')
也就是说,不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。
这样一来,就大大减少了写try...except...finally的麻烦。






2.调用堆栈
如果错误没有被捕获,它就会一直往上抛,最后被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行:

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时出错,至此,找到错误源头。

3.记录错误
如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,
就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。
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')

配置错误日志
import logging,os #导入模块
#设置文件路径,文件名,错误等级,当执行的logging的错误等级高于DEBUG,就会记录在log.txt文件中
logging.basicConfig(filename = os.path.join(os.getcwd(), 'log.txt'), level = logging.DEBUG)

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

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

如果要抛出错误,首先根据需要,可以定义一个错误的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()

在bar()函数中,我们明明已经捕获了错误,但是,打印一个ValueError!后,又把错误通过raise语句抛出去了,这不有病么?

其实这种错误处理方式不但没病,而且相当常见。捕获错误目的只是记录一下,便于后续追踪。但是,由于当前函数不知道应
该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理。好比一个员工处理不了一个问题时,就把问题抛给
他的老板,如果他的老板也处理不了,就一直往上抛,最终会抛给CEO去处理。

raise语句如果不带参数,就会把当前错误原样抛出。此外,在except中raise一个Error,还可以把一种类型的错误转化成另一种类型
try:
10 / 0
except ZeroDivisionError:
raise ValueError('input error!')
只要是合理的转换逻辑就可以,但是,决不应该把一个IOError转换成毫不相干的ValueError。

小结
Python内置的try...except...finally用来处理错误十分方便。出错时,会分析错误信息并定位错误发生的代码位置才是最关键的。

程序也可以主动抛出错误,让调用者来处理相应的错误。但是,应该在文档中写清楚可能会抛出哪些错误,以及错误产生的原因。

5.调试
第一种方法简单直接粗暴有效,就是用print()把可能有问题的变量打印出来看看:
def foo(s):
n = int(s)
print('>>> n = %d' % n)
return 10 / n

def main():
foo('0')

main()
用print()最大的坏处是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息。
所以,我们又有第二种方法。
第二种方法:断言
凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:
def foo(s):
n = int(s)
assert n != 0, 'n is zero!'
return 10 / n

def main():
foo('0')
assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。
如果断言失败,assert语句本身就会抛出AssertionError:
程序中如果到处充斥着assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-O参数来关闭assert:
关闭后,你可以把所有的assert语句当成pass来看。
第三种方法 :logging
把print()替换为logging是第3种方式,和assert比,logging不会抛出错误,而且可以输出到文件:
import logging

s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)

logging.info()就可以输出一段文本。运行,发现除了ZeroDivisionError,没有任何信息。怎么回事?
logging的好处,它允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,
logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。这样一来,你可以放心地输出不同
级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。

第四种方法 :启动Python的调试器pdb (打断点调试)
让程序以单步方式运行,可以随时查看运行状态

如果要比较爽地设置断点、单步执行,就需要一个支持调试功能的IDE。目前比较好的Python IDE有PyCharm:

6.单元测试
以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。
在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
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
为了编写单元测试,我们需要引入Python自带的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.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

编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。

以test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。

对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,
我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual():
self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等

运行单元测试
一旦编写好单元测试,我们就可以运行单元测试。最简单的运行方式是在mydict_test.py的最后加上两行代码:
if __name__ == '__main__':
unittest.main()

这样就可以把mydict_test.py当做正常的python脚本运行:
$ python3 mydict_test.py
另一种方法是在命令行通过参数-m unittest直接运行单元测试:
$ python3 -m unittest mydict_test
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK
这是推荐的做法,因为这样可以一次批量运行很多单元测试,并且,有很多工具可以自动来运行这些单元测试。


setUp与tearDown
可以在单元测试中编写两个特殊的setUp()和tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。

setUp()和tearDown()方法有什么用呢?设想你的测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,
在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码:
class TestDict(unittest.TestCase):

def setUp(self):
print('setUp...')

def tearDown(self):
print('tearDown...')
可以再次运行测试看看每个测试方法调用前后是否会打印出setUp...和tearDown...。













点赞