4异常检查和处理

286 阅读7分钟

如何控制异常 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块中

exceptexcept Exception的作用是一样的,都是处理所有的异常类型。


try...except语句的执行流程非常简单,可分为两步:

  1. 执行try语句中的代码,如果出现异常,Python会得到异常的类型
  2. 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...excepttry...except...else使用,也可以仅有tryfinally

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等会终止函数的语句。否则很容易会产生不符合预期的操作。

image-20220803150805013.png

raise手动触发异常

很多时候,系统是否要引发异常,可能需要根据应用的业务需求来决定,如果程序中的数据、执行与既定的业务需求不符,这就是一种异常。由于与业务需求不符而产生的异常,必须由程序员来决定引发,系统无法引发这种异常。

Python允许在程序中手动设置异常,使用 raise 语句即可。

raise 语句的基本语法格式为:raise [exceptionName [(reason)]]。其中,用 [] 括起来的为可选参数,其作用是指定抛出的异常名称,以及异常信息的相关描述。如果可选参数全部省略,则 raise 会把当前错误原样抛出;如果仅省略 (reason),则在抛出异常时,将不附带任何的异常描述信息。

也就是说,raise 语句有如下三种常用的用法:

  1. raise:单独一个 raise。该语句引发当前上下文中捕获的异常(比如在 except 块中),或默认引发 RuntimeError 异常。
  2. raise 异常类名称:raise 后带一个异常类名称,表示引发执行类型的异常。
  3. 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尚未实现的方法
SyntaxErrorPython 语法错误
IndentationError缩进错误
TabErrorTab 和空格混用
SystemError一般的解释器系统错误
TypeError对类型无效的操作
ValueError传入无效的参数
UnicodeErrorUnicode 相关的错误
UnicodeDecodeErrorUnicode 解码时的错误
UnicodeEncodeErrorUnicode 编码时错误
UnicodeTranslateErrorUnicode 转换时错误
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