学自廖雪峰巨佬的Python3教程:https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001431913726557e5e43e1ee8d54ee486bddc3f607afb75000
1.错误处理
可以使用错误码来表示出错,比如平时的Python程序运行成功返回0,出错返回-1,但是一旦出错,还要一级一级上报,直到某个函数可以处理该错误(比如,给用户输出一个错误信息)。
因此有try..except..finally的错误处理机制
当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是跳转到except代码块,执行完except代码块后,如果有finally语句块,则执行finally语句块,至此执行完毕。注意,就算try语句块没有报错,finally语句块也一定会被执行。except语句块也可以有多个,用于处理不同类型的错误,如果没有错误发生,可以在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的错误其实也是class,所有的错误类型都继承自BaseException,常见的错误类型和继承关系看这里:https://docs.python.org/3/library/exceptions.html#exception-hierarchy
而且,不需要在每个可能出错的地方捕获错误,只要在合适的层次去捕获错误就可以了
如果错误没有被捕获,它就会一直往上抛,最后被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
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
错误信息的第一行表示这是错误的跟踪信息
2~3行,表示调用main()出错,在代码文件err.py的第11行代码,但原因是第9行
调用bar('0')出错,在代码的第9行代码,但原因是第6行
调用return foo(s)*2出错,原因是return 10 / int(s)语句出错,根据ZeroDivisionError,找到错误源头
出错的时候,一定要分析错误的调用栈信息,才能定位错误的位置。
除了让Python解释器打印出错误的调用栈信息,我们还能用logging模块记录错误信息,前者打印异常信息之后,程序也会被结束;后者打印完信息后还会继续执行,并正常退出,而且通过配置,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')
除了捕获系统错误,还可以抛出错误,如果一个方法出现异常,但没有能力处理这种异常,就可以抛出异常。如果要抛出错误,首先根据需要,可以定义一个错误的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')
$ python3 err_raise.py
Traceback (most recent call last):
File "err_throw.py", line 11, in
foo('0')
File "err_throw.py", line 8, in foo
raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0
一般选择Python内置的错误类型,只有在必要的时候才定义我们自己的错误类型
还有另一种错误处理的方式:
# 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()中,明明已经捕获了错误,打印了提示,但是又把错误通过raise往上抛了?这是因为捕获错误的目的只是记录一下,便于后续追踪,但是,由于当前函数不知道应该怎么处理该错误,所以,最恰当的方式是继续往上抛,让顶层调用者去处理,而raise语句不带参数的话,就会把当前错误原样抛出
从最后的报错可以发现,int()是无法将字符'7.6'转换成整型数字的
2.调试
调试的第一种方法简单粗暴,就是把可能有问题的变量打印出来看看
但是凡是可以用print()来查看变量的地方,都可以用断言(assert)来替代,assert的格式是assert condition,information,意思是,assert后面跟的条件如果不成立,就抛出错误,如果有带附加信息(字符串),则输出附加信息。
logging可以替代assert,不会抛出错误,还可以输出到文件,还可以指定记录信息的级别,级别有INFO,DEBUG,WARNING,ERROR几个级别
import logging
logging.basicConfig(level=logging.INFO)
3.单元测试
单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。下面编写了一个Dict类,这个类的行为和dict一致,但是可以通过属性来访问
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
>>> d = Dict(a=1, b=2)
>>> d['a']
1
>>> d.a
1
引入Python自带的unittest模块,编写单元测试如下
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开头,否则方法不会被执行。assertEqual断言用于判断里面的两个参数是否相等,assertTrue用于判断内部表达式是否为真。另一种重要的断言就是期待抛出指定类型的Error,比如通过d['empty']访问不存在的key时,断言会抛出KeyError,而通过d.empty访问不存在的key时,期待抛出AttributeError
with self.assertRaises(KeyError):
value = d['empty']
with self.assertRaises(AttributeError):
value = d.empty
编写好单元测试之后,可以在后面加上两行代码运行
if __name__ == '__main__':
unittest.main()
另一种方法是在命令行通过参数-m unittest直接运行单元测试
可以在单元测试中编写两个特殊的的setUp()和tearDown()方法,这两个方法会分别在每调用一个测试方法的前后分别被执行,比如测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码
class TestDict(unittest.TestCase):
def setUp(self):
print('setUp...')
def tearDown(self):
print('tearDown...')
修改后的代码如下:
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def get_grade(self):
if self.score > 100 or self.score <0:
raise ValueError('invalid value: %s' % self.score)
if self.score >= 80:
return 'A'
if self.score >= 60:
return 'B'
return 'C'
其实问题就是if语句的顺序,还有就是异常数据没有抛
unittest真香
4.文档测试
通过Python内置的文档测试(doctest)模块可以直接提取注释中的代码并执行测试
# 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()
如果运行正常,说明编写的程序是正确的,就会什么输出都没有。
代码如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
def fact(n):
'''
Calculate 1*2*...*n
>>> fact(1)
1
>>> fact(10)
3628800
>>> fact(-1)
Traceback (most recent call last):
...
ValueError: no number!
'''
if n < 1:
raise ValueError('no number!')
if n == 1:
return 1
return n * fact(n - 1)
if __name__ == '__main__':
import doctest
doctest.testmod()