错误处理 try 1 2 3 4 5 6 7 8 9 10 11 12 13 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' )
假如我们认为某些代码可能回出错,就可以用try
来运行这段代码
如果执行出错,则后续代码不再执行,而是直接跳转至错误捕获代码,即except
语句块,
else
会在try执行完毕且没有抛出异常时执行
不论有无异常抛出,finally
都会执行,它会在try…except…else…都执行完后再执行
错误 python的错误也是类,所有错误类型都继承自BaseException
,所以在使用except
时,它会把定义的错误类型以及其所有子类 一并捕获
1 2 3 4 5 6 try : foo()except ValueError as e: print ('ValueError' )except UnicodeError as e: print ('UnicodeError' )
这里UnicodeError
是ValueError
的子类,所以第二个except
是永远都捕获不到的,因为第一个except已经把父类给捕获了.
点击 查看常见错误类型及继承关系
跨层调用 1 2 3 4 5 6 7 8 9 10 11 12 13 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...' )
比如上面bar()调用foo(),main()调用bar().假如foo()出错,在main()中可以直接捕获到.
异常栈 1 2 3 4 5 6 7 8 9 10 11 def foo (s ): return 10 / int (s)def bar (s ): return foo(s) * 2 def main (): bar('0' ) main()
假如错误没有被捕获,它会一直往上抛,知道被python解释器捕获,然后打印错误信息并退出程序
1 2 3 4 5 6 7 8 9 10 Traceback (most recent call last): # 告诉我们这是错误的跟踪信息 File "err.py", line 11, in <module> # 调用main()出错,在代码文件err.py的第11行,但原因在第9行 main() File "err.py", line 9, in main # 调用bar('0')出错,在代码文件err.py的第9行,但原因在第6行 bar('0') 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: division by zero # 根据错误类型ZeroDivisionError,我们判断,int(s)本身并没有出错,但是int(s)返回0,在计算10 / 0时出错,至此,找到错误源头。
这就是异常栈,出错了向大佬提问时记得把完整异常栈附上.
记录错误 python内置的logging
模块可以轻松记录错误信息,而且程序出错后会继续执行,正常退出.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import loggingdef 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' )
1 2 3 4 5 6 7 8 9 10 11 # 执行后输出如下 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
具体例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import loggingimport sys logging.basicConfig(filename='app.log' , level=logging.ERROR, format ='%(asctime)s - %(levelname)s - %(message)s' , encoding='utf-8' )def divide (x, y ): try : result = x / y return result except Exception as e: print (f"发生异常: {e} " , file=sys.stderr) logging.error(f"发生异常: {e} " , exc_info=True ) return None num1 = 10 num2 = 0 result = divide(num1, num2)if result is not None : print (f"结果: {result} " )print ('Done' )
1 2 Done 发生异常: division by zero
raise 抛出自己定义的错误 错误也是class,捕获一个错误本质上就是捕获该class的一个实例
,因此错误并不是凭空产生的,而是有意创建并抛出的.python的内置函数会抛出很多类型的错误,我们也可以自己抛出错误.(但一般来说,尽量使用python内置的错误类型)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class FooError (ValueError ): pass def foo (s ): n = int (s) if n==0 : raise FooError(f'invalid value: {s} ' ) raise 10 / n foo('0' ) >>> 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
向上抛出错误 捕获错误的目的只是记录一下,便于后续追踪.但是如果当前函数不知道如何处理这个错误,最恰当的当时继续往上抛,让顶层调用者处理.(员工处理不了的问题抛个老板,老板处理不了抛个CEO).这是很常见的一个做法.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def foo (s ): n = int (s) if n==0 : raise ValueError('invalid value: %s' % s) return 10 / ndef bar (): try : foo('0' ) except ValueError as e: print ('ValueError!' ) raise bar()
在except
中raise
一个error,还可以转换错误的类型
1 2 3 4 try : 10 / 0 except ZeroDivisionError: raise ValueError('input error!' )
调试 有时候遇到一些复杂的bug,需要知道出错时,哪些变量的值食正确的,哪些是错误的.此时就需要一套调试手段来修复.
print 最简单,最常见的手法,直接print出来
断言 用断言来代替print
1 2 3 4 def foo (s ): n = int (s) assert n != 0 , 'n is zero!' return 10 / n
assert
: 表达式n != 0
应该是true
,否则则断言失败,抛出AssertionError
1 2 3 Traceback (most recent call last): ... AssertionError: n is zero!
执行时可以用-O
来关闭断言
1 2 3 4 5 $ python -O err.py Traceback (most recent call last): ... ZeroDivisionError: division by zero# 关闭后,所有assert语句都可以看成pass
logging 通过写日志的方式记录错误
1 2 3 4 5 6 7 8 9 10 11 12 13 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
pdb python的调试器,让程序以单步方式运行,客户以随时查看运行状态.
1 2 3 4 s = '0' n = int (s)print (10 / n)
然后通过pdb启动
1 2 3 python -m pdb err.py> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>() -> s = '0'
输入l
来查看代码
1 2 3 4 5 (Pdb) l 1 # err.py 2 -> s = '0' 3 n = int(s) 4 print(10 / n)
输入n
单步执行代码
1 2 3 4 5 6 (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 <变量名>
来查看变量的值
1 2 3 4 (Pdb) p s '0' (Pdb) p n 0
q
退出
pdb.set_trace() 1 2 3 4 5 6 import pdb s = '0' n = int (s) pdb.set_trace() print (10 / n)
运行到pdb.set_trace()
暂停并进入pdb调试环境,可以用命令p
查看变量,或者命令c
继续
1 2 3 4 5 6 7 8 9 10 $ python 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
IDE 上面的方法都是土方法,最方便的就是用IDE自带的调试功能.
以vscode为例:
设置断点: 在代码行号左侧单击,出现一个红色的圆点,表示设置了一个断点.
launch.json: vscode 自动生成一个launch.json文件,用来配置环境.用来设置调试类型,启动程序,参数等.
F5
启动用调试
继续F5
执行到下一个断点
单步跳过F10
: 逐行执行代码,遇到函数调用时跳过函数内部
单步调试F11
: 逐行执行代码,遇到函数调用时进入函数内部
跳出Shift+F11
: 跳出当前函数
重新开始调试Shift+Cmd+F11
结束调试Shit+F5
单元测试 测试驱动开发(TDD: Test-Driven Development)
单元测试是对一个模块,一个函数或者一个类来进行正确性检验的测试工作.
比如对函数abs()
,我们可以编写出以下几个测试用例.
输入正数,比如1
, 1.2
, 0.99
,期待返回值与输入相同 输入负数,比如-1
,-1.2
,-0.99
,期待返回值与输入值相反 输入0
,期待返回0
输入非数值类型,比如None
,[]
,{}
,期待抛出TypeError
把上面的测试用例放到一个测试模块里,就是一个完整的单元测试.
单元测试通过,说明我们测试的这个函数能够正常工作.单元测试不通过,要么函数有bug,要么测试条件不正确,总之需要修复使单元测试通过.
下面编写一个Dict
类,行为与dict
一致,但是可以通过属性来访问:
1 2 3 4 5 >>> d = Dict (a=1 , b=2 )>>> d['a' ]1 >>> d.a1
1 2 3 4 5 6 7 8 9 10 11 12 class Dict (dict ): def __init__ (self, **kw ): super .__init__(**kw) def __getattr__ (self, key ): try : return self [key] except KeyError: raise AttributeError(f"'Dict' object has no attribute {key} " ) def __setattr__ (self, key, value ): self [key] = value
要编写单元测试,需要用到python自带的unittest
模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import unittestfrom 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
内置很多断言,常用的有:
self.assertEqual(abs(-1), 1)
: 断言函数返回结果是否与预期的一致self.assertTrue
: 验证条件是否为True,True则通过,False则不通过并抛出AssertionError
self.assertIsInstance
: 类型检查断言with self.assertRaises
: 检查抛出的错误是否与预期的一致运行单元测试的方式推荐使用如下命令:
1 2 3 4 5 6 python -m unittest mydict_test ..... ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK
可以一次批量运行多个单元测试.
setUp and tearDown 单元测试中两个特殊方法:
setUp()
: 调用一个测试方法前
执行tearDown
: 调用一个测试方法后
执行实际使用例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import unittestclass TestUserService (unittest.TestCase): def setUp (self ): self .db = Database() self .user_service = UserService(self .db) def tearDown (self ): self .db.close() def test_create_user (self ): user = self .user_service.create_user("张三" , "zhangsan@example.com" ) self .assertEqual(user.name, "张三" ) self .assertTrue(user.id > 0 ) self .assertRegex(user.email, r'^[\w\.-]+@[\w\.-]+\.\w+$' ) @unittest.skipIf(ENV == 'production' , "不在生产环境运行" ) def test_sensitive_operation (self ): pass
组织结构
与测试工具集成
1 2 3 4 5 6 7 8 9 10 11 import pytestclass TestPayment : @pytest.fixture def payment_service (self ): return PaymentService() def test_payment (self, payment_service ): with pytest.raises(InsufficientBalance): payment_service.process_payment(-100 )
文档测试 python有个doctest
模块,可以自动提取代码中的注释的示例代码来实现测试.对于注释它要符合基本格式规则才能识别:
在三引号
文档字符串中编写 使用>>>
开头表示python提示符 期望的输出直接写在新行上(不带提示符) 如果有多行输出,需要确保空白完全匹配 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 def add (a, b ): """返回两个数的和 >>> add(1, 2) 3 >>> add(-1, 1) 0 >>> add(1.5, 2.5) 4.0 """ return a + bclass MyList : """自定义列表类 >>> lst = MyList([1, 2, 3]) >>> lst.append(4) >>> lst.get_all() [1, 2, 3, 4] >>> lst.pop() 4 >>> lst.get_all() [1, 2, 3] """ def __init__ (self, items ): self .items = list (items) def append (self, item ): self .items.append(item) def pop (self ): return self .items.pop() def get_all (self ): return self .itemsif __name__ == "__main__" : import doctest doctest.testmod()
推荐的运行测试的方法与unittest
类似:
1 python -m doctest your_script.py