如何控制异常 try-except
在执行python程序时,会有出现错误的可能。导致出错的原因有一般两种:
- 语法错误:程序员编写的代码不符合python的规范,比如把print写成了printf,此种错误一旦出现会导致程序无法正常启动,但是此类错误是可以避免的。
- 异常:异常是指在程序运行的过程中由于用户的非法输入,环境的不稳定,突然断网等等不可控的因素导致程序无法正常处理,比如做除法运算时,用户输入了除数为0的式子。这些情况需要靠python提供的异常处理机制来解决。
代码的健壮性的一方面就体现在:程序是否有完备的异常处理,能够保证在程序运行时遇到非法输入,系统崩溃,突然断网等意外时可以给出对应的错误提示,使程序正常退出。
错误捕捉
比如我读了一个不存在的文件。
with open("no_file.txt", "r") as f:
print(f.read())
输出:
Traceback (most recent call last):
File "/lib/python3.10/asyncio/futures.py", line 201, in result
raise self._exception
File "/lib/python3.10/asyncio/tasks.py", line 232, in __step
result = coro.send(None)
File "/lib/python3.10/site-packages/_pyodide/_base.py", line 500, in eval_code_async
await CodeRunner(
File "/lib/python3.10/site-packages/_pyodide/_base.py", line 351, in run_async
coroutine = eval(self.code, globals, locals)
File "<exec>", line 3, in <module>
FileNotFoundError: [Errno 44] No such file or directory: 'no_file.txt'
在它的报错中有这样一个关键词 FileNotFoundError,这就是要处理的异常类型。
捕捉错误:
try:
with open("no_file.txt", "r") as f:
print(f.read())
except FileNotFoundError as e:
print(e)
with open("no_file.txt", "w") as f:
f.write("I'm no_file.txt")
print("new file 'no_file.txt' has been written")
输出:
[Errno 44] No such file or directory: 'no_file.txt'
new file 'no_file.txt' has been written
这样就不会报错,因为潜在的 FileNotFoundError 这个异常已经被 except 捕捉了起来。 而且捕捉后,还进行创建文件并写入文本的工作。
当你再点击上面的运行,会发现重复执行这段代码的话,他就可以正常打印文字到终端了。
I'm no_file.txt
python会捕获到try中的异常,并且当try中某一行出现异常后,后面的代码将不会再被执行;而是直接调用except中的代码。
try...except是python为程序员提供处理异常的一种措施;语法如下:
try:
可能出现异常的代码
except (Error1, Error2, Error3, ...) as e:
处理异常的代码
except [Exception]:
处理异常的代码
-> Error: 异常类型,一个except代码块可以同时处理多种异常类型
-> Exception : 表示所有异常类型,一般用在最后一个except块中
except和except Exception的作用是一样的,都是处理所有的异常类型。
try...except语句的执行流程非常简单,可分为两步:
- 执行try语句中的代码,如果出现异常,Python会得到异常的类型
- Python将出现的异常类型和except语句中的异常类型做对比,调用对应except语句中的代码块
处理多个异常
程序在执行某个功能的时候可能会报多种不同的异常
首先如果多种异常的处理方案是一样的话,就能在 except 这里多写几种异常种类。 它会按照正常的执行顺序,依次检测异常,报出第一个遇到的异常。
d = {"name": "f1", "age": 2}
l = [1,2,3]
v = d["gender"]
l[3] = 4
输出:
KeyError: 'gender'
KeyError: 'gender'指的是倒数第二行代码的错误
异常处理:
d = {"name": "f1", "age": 2}
l = [1,2,3]
try:
v = d["gender"]
l[3] = 4
except (KeyError, IndexError) as e:
print("key or index error for:", e)
输出:
key or index error for: 'gender'
上面这个案例,如果在原本的 d 中加上一个 gender, 让 KeyError 不报出来,它就会接着报字典的 IndexError 异常了。
如果我想让两种 error 分开来处理,比如没有 key 的时候,加一个 key,没有 index 的时候加一个 index,就需要写两个 except 。
d = {"name": "f1", "age": 2}
l = [1,2,3]
try:
v = d["gender"]
l[3] = 4
except KeyError as e:
print("key error for:", e)
d["gender"] = "x"
except IndexError as e:
print("index error for:", e)
l.append(4)
print(d)
print(l)
输出:
key error for: 'gender'
{'name': 'f1', 'age': 2, 'gender': 'x'}
[1, 2, 3]
注意:系统不会同时处理字典的 KeyError 和列表的 IndexError。因为在程序顺序执行的时候,只要报错了, 那么就会终止错误之后的代码,进入错误 回收 环节。这个回收环节在上面的案例中也就是 except 的错误处理环节。 所以其实在不改动上面代码的情况下,l 列表是没有 append(4) 的。只有当字典正常的时候,列表的报错才会触发。
try-except-else
try...except..else的使用和try...except相同,只不过多了else代码,else中的代码只有当try中的代码块没有发现异常的时候才会调用。
下面的代码,是会报错的代码,它不会进入到 else。
l = [1,2,3]
try:
l[3] = 4
except IndexError as e:
print(e)
else:
print("no error, now in else")
输出:
list assignment index out of range
下面的代码,把 l 加一个位置,就不会报错了,那么代码就会执行到 else 阶段。
l = [1,2,3,4]
try:
l[3] = 4
except IndexError as e:
print(e)
else:
print("no error, now in else")
输出:
no error, now in else
注意:else中的代码只有当try中的代码没有出现异常时才会被执行;并且else要和try…except配合使用,如果使用了else,则代码中不能没有except,否则会报错
try-except-finally
finally的功能:不管try中的代码是否有异常,最终都会调用finally中的代码
finally可以结合try...except,try...except...else使用,也可以仅有try和finally。
l = [1,2,3]
try:
l[3] = 4
except IndexError as e:
print(e)
finally:
print("reach finally")
输出:
list assignment index out of range
reach finally
l = [1,2,3,4]
try:
l[3] = 4
except IndexError as e:
print(e)
finally:
print("reach finally")
输出:
reach finally
由于没有except处理错误,python会抛出异常,但是你会发现,python在抛出异常之前先执行finally中的代码。
比如:
try:
dddd = dddddd
finally:
print("I know there is error, so what?")
输出:
I know there is error, so what?
Traceback (most recent call last):
......
NameError: name 'dddddd' is not defined
同时,还需要注意一点的是,一定要避免在finally中编写return,raise等会终止函数的语句。否则很容易会产生不符合预期的操作。
raise手动触发异常
很多时候,系统是否要引发异常,可能需要根据应用的业务需求来决定,如果程序中的数据、执行与既定的业务需求不符,这就是一种异常。由于与业务需求不符而产生的异常,必须由程序员来决定引发,系统无法引发这种异常。
Python允许在程序中手动设置异常,使用 raise 语句即可。
raise 语句的基本语法格式为:raise [exceptionName [(reason)]]。其中,用 [] 括起来的为可选参数,其作用是指定抛出的异常名称,以及异常信息的相关描述。如果可选参数全部省略,则 raise 会把当前错误原样抛出;如果仅省略 (reason),则在抛出异常时,将不附带任何的异常描述信息。
也就是说,raise 语句有如下三种常用的用法:
- raise:单独一个 raise。该语句引发当前上下文中捕获的异常(比如在 except 块中),或默认引发 RuntimeError 异常。
- raise 异常类名称:raise 后带一个异常类名称,表示引发执行类型的异常。
- raise 异常类名称(描述信息):在引发指定类型的异常的同时,附带异常的描述信息。
def no_negative(num):
if num < 0:
raise ValueError("I said no negative")
return num
print(no_negative(-1))
Traceback (most recent call last):
......
ValueError: I said no negative
Python异常错误名称表
可以 raise 的异常类
| 异常名称 | 描述 |
|---|---|
| BaseException | 所有异常的基类 |
| SystemExit | 解释器请求退出 |
| KeyboardInterrupt | 用户中断执行(通常是输入^C) |
| Exception | 常规错误的基类 |
| StopIteration | 迭代器没有更多的值 |
| GeneratorExit | 生成器(generator)发生异常来通知退出 |
| StandardError | 所有的内建标准异常的基类 |
| ArithmeticError | 所有数值计算错误的基类 |
| FloatingPointError | 浮点计算错误 |
| OverflowError | 数值运算超出最大限制 |
| ZeroDivisionError | 除(或取模)零 (所有数据类型) |
| AssertionError | 断言语句失败 |
| AttributeError | 对象没有这个属性 |
| EOFError | 没有内建输入,到达EOF 标记 |
| EnvironmentError | 操作系统错误的基类 |
| IOError | 输入/输出操作失败 |
| OSError | 操作系统错误 |
| WindowsError | 系统调用失败 |
| ImportError | 导入模块/对象失败 |
| LookupError | 无效数据查询的基类 |
| IndexError | 序列中没有此索引(index) |
| KeyError | 映射中没有这个键 |
| MemoryError | 内存溢出错误(对于Python 解释器不是致命的) |
| NameError | 未声明/初始化对象 (没有属性) |
| UnboundLocalError | 访问未初始化的本地变量 |
| ReferenceError | 弱引用(Weak reference)试图访问已经垃圾回收了的对象 |
| RuntimeError | 一般的运行时错误 |
| NotImplementedError | 尚未实现的方法 |
| SyntaxError | Python 语法错误 |
| IndentationError | 缩进错误 |
| TabError | Tab 和空格混用 |
| SystemError | 一般的解释器系统错误 |
| TypeError | 对类型无效的操作 |
| ValueError | 传入无效的参数 |
| UnicodeError | Unicode 相关的错误 |
| UnicodeDecodeError | Unicode 解码时的错误 |
| UnicodeEncodeError | Unicode 编码时错误 |
| UnicodeTranslateError | Unicode 转换时错误 |
| Warning | 警告的基类 |
| DeprecationWarning | 关于被弃用的特征的警告 |
| FutureWarning | 关于构造将来语义会有改变的警告 |
| OverflowWarning | 旧的关于自动提升为长整型(long)的警告 |
| PendingDeprecationWarning | 关于特性将会被废弃的警告 |
| RuntimeWarning | 可疑的运行时行为(runtime behavior)的警告 |
| SyntaxWarning | 可疑的语法的警告 |
| UserWarning | 用户代码生成的警告 |
单元测试
在 Python 中,常用一个原生的 unittest 做单元测试。
- 一个
class继承unittest.TestCase类,即是一个个具体的TestCase(类方法名称必须以test开头,否则不能被unittest识别) - 每一个用例执行的结果的标识,成功是. ,失败为F,出错是E
- 每一个测试以test01、test02…依次写下去,unittest才可按照编号执行
- versity参数控制输出结果,0是简单报告、1是一般报告、2是详情报告。
- 用
setUp()、terUpClass()以及tearDownClass()可以在用例执行前布置环境,以及在用例执行后清理环境。 - 参数中加
stream,可以讲报告输出到文件:可以用HTMLTestRunner输出html报告。 - 多个单元的测试用例集合在一起,就是TestSuite。
例如:
import unittest
def my_div(a, b):
return a / b
class TestFunc(unittest.TestCase): # 继承了 unittest.TestCase,并且类中的函数需要以test开头,方可执行.
def test_div(self):
self.assertEqual(2, my_div(2,1)) # assertEqual(a, b):检查a == b,返回True或False
self.assertEqual(-2, my_div(2,-1))
if __name__ == "__main__":
unittest.main()
输出:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
**注意:**测试用例的名称要以test开头
if \__name__ == '\_\_main__': 的作用:
一个python文件通常有两种使用方法,第一是作为脚本直接执行,第二是 import 到其他的 python 脚本中被调用(模块重用)执行。因此 if name == 'main': 的作用就是控制这两种情况执行代码的过程,在if \__name__ == '\_\_main__': 下的代码只有在第一种情况下(即文件作为脚本直接执行)才会被执行,而 import 到其他脚本中是不会被执行的。
上面这个没有问题。但是当再测试另一个除以零的 case 的时候,它就会报出问题。
import unittest
def my_div(a, b):
return a / b
class TestFunc(unittest.TestCase):
def test_div(self):
self.assertEqual(1, my_div(2,0))
if __name__ == "__main__":
unittest.main()
输出:
E
======================================================================
ERROR: test_div (__main__.TestFunc)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<exec>", line 7, in test_div
File "<exec>", line 4, in my_div
ZeroDivisionError: division by zero
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
unittest 规范
首先 unittest 不会被其他人使用到,纯粹是为了验证自己写的代码有没有问题的方式。
另外,可以按照 unittest 当中的 case 为蓝本,去完善原函数的功能。 就好像有了一个目标,你要为了这个目标去开发功能一样。
对多个功能进行测试:
def my_func1(a):
if a == 1:
return 2
elif a == -1:
return 3
else:
return 1
def my_func2(b):
if b != "yes":
raise ValueError("you can only say yes!")
else:
return True
class TestFunc(unittest.TestCase):
def test_func1(self):
self.assertEqual(2, my_func1(1))
self.assertEqual(3, my_func1(-1))
for i in range(-100, 100):
if i == 1 or i == -1:
continue
self.assertEqual(1, my_func1(i))
def test_func2(self):
self.assertTrue(my_func2("yes"))
with self.assertRaises(ValueError):
my_func2("nononono")
if __name__ == "__main__":
unittest.main()
输出:
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
用 Python 命令执行测试
注意,有些人可能会比较喜欢通过 Python 的指令来运行测试,比如下面这样。
python -m unittest tests.py
能测哪些 assert
| assert | 含义 |
|---|---|
| assertEqual(a, b) | a == b |
| assertNotEqual(a, b) | a != b |
| assertTrue(condition) | condition 是不是 True |
| assertFalse(condition) | condition 是不是 False |
| assertGreater(a, b) | a > b |
| assertGreaterThan(a, b) | a >= b |
| assertLess(a, b) | a < b |
| assertLessEqual(a, b) | a <= b |
| assertIs(a, b) | a is b,a 和 b 是不是同一对象 |
| assertIsNot(a, b) | a is not b,a 和 b 是不是不同对象 |
| assertIsNone(a) | a is None,a 是不是 None |
| assertIsNotNone(a) | a is not None,a 不是 None? |
| assertIn(a, b) | a in b, a 在 b 里面? |
| assertNotIn(a, b) | a not in b,a 不在 b 里? |
| assertRaises(err) | 通常和 with 一起用,判断 with 里的功能是否会报错(上面练习有用到过) |
测单独的功能
两种方法
1.复杂的方法
在你的 test.py 中,将代码最下边的 unittest.main() 替换成下面这段代码中那些 TestSuite() 和 TextTestRunner() 部分。(灵活性较差)
class TestFunc(unittest.TestCase):
def test_func1(self):
self.assertEqual(2, my_func1(1))
self.assertEqual(3, my_func1(-1))
for i in range(-100, 100):
if i == 1 or i == -1:
continue
self.assertEqual(1, my_func1(i))
def test_func2(self):
self.assertTrue(my_func2("yes"))
with self.assertRaises(ValueError):
my_func2("nononono")
# 定义一个 suite 替换 unittest.main()
suite = unittest.TestSuite()
suite.addTest(TestFunc('test_func1'))
unittest.TextTestRunner().run(suite)
输出:
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
<unittest.runner.TextTestResult run=1 errors=0 failures=0>
2.简单的方法
接用 Python 的命令来执行不同的 test。
python -m unittest tests.TestFunc.test_func2