第七章 错误与异常处理
1. 错误与异常
在Python(以及许多编程语言)中,错误和异常是紧密相关但又有所区别的概念。简单来说,错误通常指程序无法正常运行的问题,而异常则是程序运行过程中出现的、可以被捕获和处理的事件。
1.1 错误(Error)
错误通常是程序在语法层面或逻辑层面的严重问题,导致程序无法被解释器执行或继续运行。常见的错误有两类:
语法错误(Syntax Error)
定义:代码不符合Python语法规则,解释器无法解析。例如缺少冒号、括号不匹配、拼错关键字等。特点:程序根本无法开始运行,解释器会在第一处语法错误处报错并停止。
逻辑错误(Logical Error)
定义:代码语法正确,但逻辑上不正确,导致程序结果不符合预期。例如除零、无限循环、算法错误等。特点:程序可以运行,但结果错误或崩溃(崩溃时会引发异常)。
在Python文档中,错误常特指语法错误,因为语法错误是在编译/解析阶段就被发现的。而逻辑错误往往在运行时表现为异常。
if True# 缺少冒号
print("Hello")
# 运行时会报:SyntaxError: expected ':'
1.2 异常(Exception)
异常是程序运行期间发生的、干扰正常指令流程的事件。当Python解释器遇到无法处理的运行时错误时,它会创建一个异常对象并抛出(raise)。如果程序没有捕获这个异常,程序就会终止并显示错误信息(即“回溯”Traceback)。
即:代码在语法上没问题,但执行过程中出现了问题。— 可以通过异常处理机制解决。
# 异常:代码在语法上没问题,但执行过程中出现了问题。———— 可以通过异常处理机制解决
# 一些开发中常见的异常:
# 1.ZeroDivisionError:当除数为 0 时触发。
num1 = 100
num2 = 0
result = num1 / num2
# 执行结果:ZeroDivisionError: division by zero
# 2.TypeError:当操作的数据类型不正确或不兼容时触发。
result = '10' + 5
# 执行结果:TypeError: can only concatenate str (not "int") to str
# 3.AttributeError: 当对象没有指定的属性或方法时触发。
# 演示1
class Person:
def__init__(self, name, age):
self.name = name
self.age = age
p1 = Person('张三', 18)
print(p1.name)
print(p1.age)
print(p1.gender)
# 执行结果:AttributeError: 'Person' object has no attribute 'gender'
# 演示2
nums = [10, 20, 30]
nums.add(40)
# 执行结果:AttributeError: 'list' object has no attribute 'add'
# 4.IndexError:当索引超出范围(索引越界)时触发。
nums = [10, 20, 30, 40]
print(nums[4])
# 执行结果:IndexError: list index out of range
# 5.NameError:当使用了不存在的变量时触发。
print(school)
# 执行结果:NameError: name 'school' is not defined
# 6.KeyError:当访问字典中不存在的 key 时触发。
person = {'name':'张三', 'age':18}
print(person['gender'])
# 执行结果:KeyError: 'gender'
# 7.ValueError:当值不合法,但类型正确时触发。
int('hello')
# 执行结果:ValueError: invalid literal for int() with base 10: 'hello'
Python 中异常类的继承关系(层级关系)如下(了解即可):
其中:BaseException是所有异常类的父类,Exception中包含的是开发中常见的业务异常。
BaseException
├── BaseExceptionGroup
├── GeneratorExit
├── KeyboardInterrupt
├── SystemExit
└── Exception
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── AssertionError
├── AttributeError
├── BufferError
├── EOFError
├── ExceptionGroup [BaseExceptionGroup]
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── MemoryError
├── NameError
│ └── UnboundLocalError
├── OSError
│ ├── BlockingIOError
│ ├── ChildProcessError
│ ├── ConnectionError
│ │ ├── BrokenPipeError
│ │ ├── ConnectionAbortedError
│ │ ├── ConnectionRefusedError
│ │ └── ConnectionResetError
│ ├── FileExistsError
│ ├── FileNotFoundError
│ ├── InterruptedError
│ ├── IsADirectoryError
│ ├── NotADirectoryError
│ ├── PermissionError
│ ├── ProcessLookupError
│ └── TimeoutError
├── ReferenceError
├── RuntimeError
│ ├── NotImplementedError
│ ├── PythonFinalizationError
│ └── RecursionError
├── StopAsyncIteration
├── StopIteration
├── SyntaxError
│ └── IndentationError
│ └── TabError
├── SystemError
├── TypeError
├── ValueError
│ └── UnicodeError
│ ├── UnicodeDecodeError
│ ├── UnicodeEncodeError
│ └── UnicodeTranslateError
└── Warning
├── BytesWarning
├── DeprecationWarning
├── EncodingWarning
├── FutureWarning
├── ImportWarning
├── PendingDeprecationWarning
├── ResourceWarning
├── RuntimeWarning
├── SyntaxWarning
├── UnicodeWarning
└── UserWarning
2. 异常处理
2.1 为什么需要进行异常处理
- 程序运行过程中出现的异常,如果得不到处理,那程序就会立即崩溃,导致后续代码无法执行。
- 异常处理不是让异常消失,而是将异常捕获到,随后根据异常的具体情况,来执行指定的逻辑。
print('欢迎使用本程序')
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
result = a / b
print(f'{a}除以{b}的结果是:{result}')
print('*******我是后续的其它逻辑1*******')
print('*******我是后续的其它逻辑2*******')
# 以上程序a,b输入正确数字可以得到结果,如果输入字母,中文,或者b=0等,就会报错。
2.2 异常处理(初级方式)
核心规则如下:
将可能出现异常的代码放在try中,出现异常后的处理代码写在except中。
如果try中的代码出现异常,那try中的后续代码不会执行,并自动跳转到except中。
如果try中的代码没有异常,那except中的代码就不会执行。
无论是否发生异常,try-except后面的代码都会继续执行。
直接写except捕获到Python中所有的异常 ———— 实际开发中不推荐这样做,因为这种异常捕捉范围太大,不便于程序代码的排查。
# 异常处理(初级):
# 1.将可能出现异常的代码放在 try 中,出现异常后的处理代码写在 except 中。
# 2.如果 try 中的代码出现异常,那 try 中的后续代码将不会执行,并自动跳转到 except 中处理异常。
# 3.如果 try 中的代码没有异常,那 except 中的代码就不会执行。
# 4.无论是否发生异常,try-except 后面的代码都会继续执行。
# 5.直接写 except 会捕获到 Python 中所有的异常 ———— 实际开发中不推荐这样做,因为这种异常捕捉范围太大,不便于程序代码的排查。
print('欢迎使用本程序')
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
result = a / b
print(f'{a}除以{b}的结果是:{result}')
except:
print('抱歉,程序出现了异常!')
print('*******我是后续的其它逻辑1*******')
print('*******我是后续的其它逻辑2*******')
# 以上程序a,b输入正确数字可以得到结果,如果输入字母,中文,或者b=0等,就会出现异常信息,但不会影响程序的后续执行。
2.3 捕获指定类型的异常
# 异常处理(捕获指定的类型的异常)
print('欢迎使用本程序')
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
result = a / b
print(f'{a}除以{b}的结果是:{result}')
except ZeroDivisionError:
print('程序异常:0不能作为除数!') # 当b = 0时,会抛出此类异常,程序会走此处异常。
except ValueError:
print('程序异常:您输入的必须是数字!') # 当a或者b输入的不是数字时,程序会走此处抛出对应异常。
print('*******我是后续的其它逻辑1*******')
print('*******我是后续的其它逻辑2*******')
2.4 异常类之间的继承关系
# 验证一下异常类之间的继承关系(和异常层级结构对比一目了然)
# issubclass(class, classinfo) 是Python内置函数,用于判断一个类是否是另一个类或一组类(由classinfo指定)的子类。
print(issubclass(ZeroDivisionError, ArithmeticError)) # True
print(issubclass(ZeroDivisionError, Exception)) # True
print(issubclass(ValueError, Exception)) # True
print(issubclass(KeyboardInterrupt, Exception)) # False
print(issubclass(KeyboardInterrupt, BaseException)) # True
2.5 多个异常处理
多个异常从上往下匹配,匹配成功后不再向下匹配。
print('欢迎使用本程序')
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
# print(x) # 此出代码可以打开和注释看异常执行效果
result = a / b
print(f'{a}除以{b}的结果是:{result}')
except ZeroDivisionError:
print('程序异常:0不能作为除数!') # 当b=0时,匹配抛出此异常,下面的异常不会执行。
except ValueError:
print('程序异常:您输入的必须是数字!') # 当a或者b不是数字时,匹配抛出此异常,下面的异常不会执行。
except Exception:
print('程序异常!') # 所以异常为匹配到,抛出此异常,该异常范围最大。
print('*******我是后续的其它逻辑1*******')
print('*******我是后续的其它逻辑2*******')
2.6 获取异常的具体信息
通过e变量,可以获取异常相关的信息,也可以借助traceback去格式化异常信息。
# 获取异常的具体信息
print('欢迎使用本程序')
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
print(x) # 此出代码可以打开和注释看异常执行效果
result = a / b
print(f'{a}除以{b}的结果是:{result}')
except ZeroDivisionError:
print('程序异常:0不能作为除数!')
except ValueError:
print('程序异常:您输入的必须是数字!')
except Exception as e:
print(f'程序异常,异常信息:{e}')
print(f'程序异常,异常类型:{type(e)}')
print(f'程序异常,异常参数:{e.args}')
print(f'程序异常,异常的文件:{e.__traceback__.tb_frame.f_code.co_filename}')
print(f'程序异常,异常的具体行数:{e.__traceback__.tb_lineno}')
# 通过 traceback 来回溯异常
import traceback
print(traceback.format_exc()) # NameError: name 'x' is not defined
print('*******我是后续的其它逻辑1*******')
print('*******我是后续的其它逻辑2*******')
2.7 一个except捕获不同的异常
# 一个 except,也可以捕获不同的异常
print('欢迎使用本程序')
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
print(x) # 此出代码可以打开和注释看异常执行效果
result = a / b
print(f'{a}除以{b}的结果是:{result}')
except (ZeroDivisionError, ValueError, Exception) as e:
if isinstance(e, ZeroDivisionError):
print('程序异常:0不能作为除数!')
elif isinstance(e, ValueError):
print('程序异常:您输入的必须是数字!')
else:
print(f'程序异常:{e}')
print('*******我是后续的其它逻辑1*******')
print('*******我是后续的其它逻辑2*******')
2.8 异常完整写法
except:出现异常时的处理(出现异常时怎么补救)。
# 异常处理的完整写法:
# 1.try:尝试去做可能会出现异常的事情
# 2.except:出现异常时的处理(出现异常时怎么补救)
# 3.else:如果一切顺利(没有异常出现)要做的事
# 4.finally:无论有没有异常,都要做的事
print('欢迎使用本程序')
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
result = a / b
print(f'{a}除以{b}的结果是:{result}')
except (ZeroDivisionError, ValueError, Exception) as e:
if isinstance(e, ZeroDivisionError):
print('程序异常:0不能作为除数!')
elif isinstance(e, ValueError):
print('程序异常:您输入的必须是数字!')
else:
print(f'程序异常:{e}')
else:
print('挺好的,try中的代码没有任何异常!')
finally:
print('无论有没有异常,我的计算都结束了!')
print('*******我是后续的其它逻辑1*******')
print('*******我是后续的其它逻辑2*******')
3. 手动抛出异常
当程序遇到不符合预期情况时,可以使用raise语句手动触发(抛出)异常。
在 Python 中,raise 语句用于手动抛出异常即主动触发一个异常,中断当前代码的执行流程,并将异常传递给调用者。根据使用场景,raise 有多种常见用法:
3.1 抛出一个指定的异常
def withdraw(amount):
if amount < 0:
raise ValueError("取款金额不能为负数") # 抛出异常
print(f"取款 {amount} 元")
# 调用时触发异常
withdraw(-100) # 抛出 ValueError: 取款金额不能为负数
- 异常类可以是 Python 内置的(如
ValueError、TypeError),也可以是自定义的异常类(继承自 Exception)。
3.2 在 except 块中重新抛出当前异常
如果在捕获异常后,你只做部分处理(比如记录日志),但希望异常继续向上传递,可以使用不带参数的 raise
这种方式会保留原始的异常堆栈信息,非常有利于调试。
try:
x = int("abc")
except ValueError as e:
print("记录日志:转换失败")
raise# 重新抛出刚刚捕获的异常,保留原始调用栈
# 执行结果:
# 记录日志:转换失败
# Traceback (most recent call last):
# ValueError: invalid literal for int() with base 10: 'abc'
3.3 抛出另一个异常(异常链 - raise ... from ...)
有时在捕获一个异常后,你想将其包装成另一种异常抛出,同时保留原始异常的信息。可以使用 raise ... from ...
try:
1 / 0
except ZeroDivisionError as e:
raise ValueError("计算时发生除零错误") from e
# 执行结果:
# ValueError: 计算时发生除零错误
并且会附带 "The above exception was the direct cause of the following exception" 的说明,指出原始异常。
如果不想保留异常链,可以使用 from None 来抑制原始异常的显示:
try:
1 / 0
except ZeroDivisionError as e:
raise ValueError("新异常") from None
# 执行结果:ValueError: 新异常
3.4 不带参数的 raise(只能在 except 块中使用)
它重新抛出当前正在处理的异常,相当于 raise e 但更简洁。
3.5 抛出一个异常对象
除了抛出异常类,你也可以先创建异常对象再抛出:
# 这等价于 raise ValueError("自定义错误信息")。
err = ValueError("自定义错误信息")
raise err
# 执行结果如下:
# ValueError: 自定义错误信息
3.6 总结
raise 的核心用途是:
- 在异常处理后重新抛出(保留原始异常或包装为新异常)。
4. 异常的传递机制
在 Python 中,异常的传递机制指的是当程序运行过程中发生异常时,解释器如何沿着函数的调用链(调用栈)向上查找能够处理该异常的 except 块,直到找到合适的处理器或到达程序顶层。理解这一机制有助于编写健壮的异常处理代码。
如果异常没有被当前代码块所捕获处理,那该异常就会沿着调用链,逐层传递给其调用者。
如果所有调用者,都没有捕获该异常,那最终程序将因未处理异常而意外终止。
4.1 异常传递的基本过程
当一个异常被 raise(无论是 Python 解释器自动触发,还是手动抛出)时,程序会立即中断当前代码的正常执行,并开始沿调用栈向上回溯:
在当前函数内部查找解释器检查当前函数中是否有 try...except 语句包围了发生异常的代码,并且 except 块能够匹配该异常类型。
如果找到匹配的 except,则执行该 except 块中的代码,然后异常被处理,程序从 try...except 结构之后的代码继续执行(除非 except 块中再次抛出异常)。
如果当前函数中没有合适的 try...except,则将异常传递给调用当前函数的上一级函数。
在上一级函数中继续查找上一级函数会重复同样的过程:检查该函数中是否有包围函数调用的 try...except。
到达程序顶层(模块层级)如果异常一直传递到程序的顶层(即全局作用域)仍未被捕获,Python 解释器将终止程序执行,并在标准错误输出中打印异常的回溯信息(Traceback),显示异常发生的位置和调用链。
def func_c():
print("进入 func_c")
# 这里发生一个除零异常
result = 10 / 0# ZeroDivisionError
print("func_c 结束") # 这行不会执行
def func_b():
print("进入 func_b")
func_c() # 调用 func_c
print("func_b 结束") # 也不会执行(除非异常被捕获)
def func_a():
print("进入 func_a")
try:
func_b() # 调用 func_b
except ZeroDivisionError:
print("在 func_a 中捕获到除零异常")
print("func_a 结束")
func_a()
# 程序执行结果如下:
# 进入 func_a
# 进入 func_b
# 进入 func_c
# 在 func_a 中捕获到除零异常
# func_a 结束
# 异常的传递机制:
# 1.如果异常没有被当前代码块所捕获处理,那该异常就会沿着调用链,逐层传递给其调用者。
# 2.如果所有调用者,都没有捕获该异常,那最终程序将因【未处理异常】而意外终止。
def test1():
print('******test1开始******')
result = '100' + 100
print('******test1结束******')
def test2():
print('******test2开始******')
try:
test1()
except Exception as e:
print(f'程序异常:{e}')
print('******test2结束******')
def test3():
print('******test3开始******')
test2()
print('******test3结束******')
test3()
# 程序执行结果如下:
# ******test3开始******
# ******test2开始******
# ******test1开始******
# 程序异常:can only concatenate str (not "int") to str
# ******test2结束******
# ******test3结束******
4.2 异常传递的关键点
except 可以指定捕获的异常类型(可以是单个类,也可以是元组)。只有异常的类型与指定的类型相匹配(或为其子类)时,才会被捕获。
try 语句可以有多个 except 块,解释器会·按顺序·检查,执行第一个匹配的 except。因此,更具体的异常类型应该放在前面,通用的(如 Exception)放在后面。
finally:无论是否发生异常,也不论异常是否被捕获,finally 块中的代码总会执行(除非在 finally 之前程序被强制终止,如 os._exit())。finally 常用于释放资源(关闭文件、网络连接等)。但需要注意的是,如果在 finally 中使用了
finally 常用于释放资源(关闭文件、网络连接等)。但需要注意的是,如果在 finally 中使用了 return 或抛出了新的异常,可能会影响原异常的传递。
- 在
except 块中,可以使用 raise(不带参数)重新抛出当前捕获的异常,让异常继续向上传递。这常用于记录日志后,仍希望上层处理。
- 通过
raise ... from ... 可以显式地链接异常,表明当前异常是由另一个异常引起的。在传递过程中,回溯信息会保留原始异常链。
4.3 没有捕获时的最终结果
如果异常最终未被任何 except 捕获,解释器会打印 Traceback 并终止程序。
def outer():
inner()
def inner():
1 / 0
outer()
# 执行结果类似如下:
# Traceback (most recent call last):
# File "example.py", line 7, in <module>
# outer()
# File "example.py", line 2, in outer
# inner()
# File "example.py", line 5, in inner
# 1 / 0
# ZeroDivisionError: division by zero
回溯信息清晰地展示了异常从 inner 到 outer 再到模块层级的传递路径。
4.4 总结
Python 的异常传递机制是一种基于调用栈的自动回溯,它使得错误处理可以集中在合适的层次进行,而不必在每个函数中手动检查错误码。理解这一机制能帮助你设计更清晰的错误处理策略,并在调试时快速定位问题根源。
5. 自定义异常类
由开发人员自己定义一个异常类,用来表示代码中更具体、更有业务含义的异常。
具体规则:定义一个类(类名通常以Error结尾),继承Exception类或它的子类。
# 自定义异常类:
# 1.由开发人员自己定义一个异常类,用来表示代码中“更具体、更有业务含义”的异常。
# 2.具体规则:定义一个类(类名通常以 Error 结尾),继承 Exception 类或它的子类。
class SchoolNameError(Exception):
def __init__(self, msg):
super().__init__('【姓名长度异常】' + msg)
def check_school_name(name):
if len(name) > 10:
raise SchoolNameError('姓名长度过长')
else:
print('姓名长度是合法的')
try:
check_school_name('zhangsansansansansan')
except SchoolNameError as e:
print(f'程序异常:{e}') # 程序异常:【姓名长度异常】姓名长度过长