注:学习资料来源<尚硅谷>
视频:点击跳转
代码仓库:pythonStudy
在 Python 开发过程中,程序并不总能按照预期顺利执行。造成程序中断的原因,通常可以归纳为两类:错误(Error) 与 异常(Exception)。
很多初学者会把这两个概念混为一谈,但从 Python 的执行机制来看,它们在触发阶段、处理方式以及工程实践中的意义都存在明显差异。本文将围绕以下几个方面,系统梳理 Python 中的错误与异常机制:
错误与异常的本质区别
常见异常类型及其触发场景
try-except-else-finally的完整异常处理体系raise手动抛出异常的应用场景异常在函数调用链中的传递机制
如何设计和使用自定义异常类
一、错误与异常:概念区分
1. 什么是错误(Error)
所谓“错误”,通常指的是 语法层面的错误,也就是代码在被解释器解析时就已经不符合 Python 语法规则,因此程序根本无法进入正常执行阶段。
例如:
age = 18
if age >= 18
print('成年人')上述代码中,if 语句后缺少冒号,属于典型的 SyntaxError(语法错误)。
这类问题的特点是:
发生在代码解析阶段,而非运行阶段
程序无法正常启动或继续解释执行
不能通过普通的异常处理机制进行补救
唯一有效的解决方式是:修正源代码
2. 什么是异常(Exception)
与错误不同,异常是指:
代码在语法上完全正确,但在运行过程中,由于数据、对象状态或外部环境等因素导致程序出现非预期情况。
例如:
num1 = 100
num2 = 0
result = num1 / num2这段代码语法没有任何问题,但由于除数为 0,运行时会触发 ZeroDivisionError。
异常的典型特点包括:
发生在程序运行阶段
可以被捕获、处理、记录和恢复
合理的异常处理有助于提升程序健壮性和用户体验
二、常见异常类型及触发场景
Python 内置了丰富的异常类型,用于描述不同的运行时故障。下面列举开发中最常见的几类。
1. ZeroDivisionError
当除数为 0 时触发。
num1 = 100
num2 = 0
result = num1 / num22. TypeError
当参与运算或调用的对象类型不兼容时触发。
result = '10' + 5字符串与整数不能直接相加,因此会抛出类型异常。
3. AttributeError
当对象不存在某个属性或方法时触发。
示例一:访问不存在的属性
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person('张三', 18)
print(p1.gender)示例二:调用不存在的方法
nums = [10, 20, 30]
nums.add(40)list 类型并没有 add() 方法,因此会触发属性异常。
4. IndexError
当使用的索引超出序列有效范围时触发。
nums = [10, 20, 30, 40]
print(nums[4])由于列表下标范围是 0 ~ 3,访问 4 会越界。
5. NameError
当使用了未定义的变量名时触发。
print(school)如果变量 school 尚未定义,解释器将无法解析该名称。
6. KeyError
当访问字典中不存在的键时触发。
person = {'name': '张三', 'age': 18}
print(person['gender'])字典中没有 gender 这个键,因此会报错。
7. ValueError
当数据类型本身正确,但具体的值不符合要求时触发。
int('hello')这里传入的是字符串,类型没问题,但字符串内容无法转换为整数。
三、Python 异常体系简析
Python 的异常类具有严格的继承体系。理解这一点,有助于我们设计更合理的异常捕获逻辑。
从结构上看:
BaseException是所有异常的根类Exception是绝大多数业务开发中常见异常的直接父类各类具体异常,如
ValueError、TypeError、KeyError等,均继承自Exception
例如:
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这说明:
ZeroDivisionError是ArithmeticError的子类大部分运行时业务异常都属于
Exception体系某些系统级中断,如
KeyboardInterrupt,并不属于普通业务异常范畴
在实际开发中,通常建议优先捕获 Exception 及其子类,而不要轻易覆盖更顶层的 BaseException。
四、为什么需要异常处理
如果程序运行过程中出现异常,而没有进行处理,那么程序会立即终止,后续逻辑无法继续执行。
例如:
print('欢迎使用本程序')
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
result = a / b
print(f'{a}除以{b}的结果是:{result}')
print('后续逻辑1')
print('后续逻辑2')如果用户输入了非数字内容,或者输入的第二个数为 0,程序都会在中途崩溃,后面的逻辑完全无法执行。
因此,异常处理的意义并不是“消灭异常”,而是:
捕获异常
识别异常类型
给出明确提示
必要时进行降级处理或恢复
保证程序整体流程尽量稳定
五、异常处理基础:try-except
1. 基本语法结构
try:
# 可能发生异常的代码
except:
# 异常发生后的处理逻辑示例:
print('欢迎使用本程序')
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
result = a / b
print(f'{a}除以{b}的结果是:{result}')
except:
print('抱歉,程序出现了异常!')
print('后续逻辑1')
print('后续逻辑2')2. 执行机制说明
try中如果没有异常,则except不执行try中一旦发生异常,其后续代码立即中断程序会跳转到
except中执行对应处理逻辑无论是否发生异常,
try-except后面的代码通常仍可继续执行
六、捕获指定类型的异常
在工程实践中,直接使用裸 except 并不是理想做法。因为它会模糊异常类型,降低排错效率。
更推荐的做法是:按类型精确捕获。
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
result = a / b
print(f'{a}除以{b}的结果是:{result}')
except ZeroDivisionError:
print('程序异常:0不能作为除数!')
except ValueError:
print('程序异常:您输入的必须是数字!')这样做的优势在于:
异常处理语义更明确
便于针对不同故障采取不同策略
更符合工程化代码规范
七、多个 except 的匹配规则
当一个 try 语句后跟随多个 except 分支时,Python 会按 从上到下 的顺序匹配。
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
print(x)
result = a / b
except ZeroDivisionError:
print('程序异常:0不能作为除数!')
except ValueError:
print('程序异常:您输入的必须是数字!')
except Exception:
print('程序异常!')匹配规则是:
一旦某个
except分支匹配成功,后续分支将不再继续匹配因此,应当把 更具体的异常类型写在前面
把 更通用的异常类型(如
Exception)放在最后作为兜底
八、获取异常对象与诊断信息
在捕获异常时,我们通常不仅要知道“发生了异常”,还要知道:
异常类型
异常信息
发生位置
调用堆栈
这时可以使用 as e 获取异常对象:
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
print(x)
result = a / b
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
try:
print(1 / 0)
except Exception:
print(traceback.format_exc())这在日志记录、线上排错、异常监控等场景中非常实用。
九、一个 except 捕获多个异常
如果多个异常的处理逻辑接近,也可以通过元组形式统一捕获:
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
print(x)
result = a / b
except (ZeroDivisionError, ValueError, Exception) as e:
if isinstance(e, ZeroDivisionError):
print('程序异常:0不能作为除数!')
elif isinstance(e, ValueError):
print('程序异常:您输入的必须是数字!')
else:
print(f'程序异常:{e}')这种方式适用于:
多个异常的处理流程大致一致
希望统一入口处理,再在内部进行细分判断
不过,如果不同异常的业务含义差异较大,仍然建议分开写多个 except,可读性通常更强。
十、完整异常处理结构:try-except-else-finally
Python 提供了一套完整的异常处理语法:
try:
# 可能出现异常的代码
except:
# 出现异常时的处理逻辑
else:
# 没有异常时执行
finally:
# 无论是否异常都执行示例:
try:
a = int(input('请输入第一个数:'))
b = int(input('请输入第二个数:'))
result = a / b
except (ZeroDivisionError, ValueError, Exception) as e:
if isinstance(e, ZeroDivisionError):
print('程序异常:0不能作为除数!')
elif isinstance(e, ValueError):
print('程序异常:您输入的必须是数字!')
else:
print(f'程序异常:{e}')
else:
print(f'计算结果:{result}')
print('try中的代码执行成功,没有发生异常。')
finally:
print('无论有没有异常,这段清理逻辑都会执行。')各部分职责划分
try:放置高风险代码except:处理异常分支else:处理成功分支finally:执行资源释放、关闭文件、断开连接等收尾操作
这是一种非常典型的工程化异常处理结构。
十一、手动抛出异常:raise
异常不一定都来自 Python 解释器自动触发。在很多业务场景中,数据虽然语法合法,但不符合业务规则,此时可以主动抛出异常。
例如年龄校验:
print('欢迎使用年龄判断系统')
try:
age = int(input('请输入你的年龄:'))
if 18 <= age <= 120:
print('成年')
elif 0 <= age < 18:
print('未成年')
else:
raise ValueError('年龄应该为0~120的整数')
except Exception as e:
print(f'程序异常:{e}')这里的核心思想是:
一旦检测到非法业务数据,应主动终止当前流程,并显式告知错误原因。
raise 常用于:
参数校验
数据格式校验
权限校验
状态检查
业务规则约束
十二、异常的传递机制
如果异常在当前函数内部没有被捕获,它不会凭空消失,而是会沿着 函数调用链逐层向上传递,直到被某一层捕获处理。
示例:
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()输出结果表明:
异常首先发生在
test1test1本身没有处理异常传递到
test2test2捕获后,程序继续执行因此
test3也能顺利结束
这一机制的意义
异常传递机制使得程序可以采用分层处理策略:
底层函数专注于业务执行
中间层决定是否转换异常、补充上下文
顶层统一进行日志记录、用户提示和故障兜底
这也是很多大型项目采用统一异常处理中间件或全局异常处理器的基础。
十三、自定义异常类
在实际开发中,内置异常类型有时无法准确表达某些业务错误。此时就可以通过 自定义异常类 来增强语义表达能力。
1. 自定义异常的基本规则
通常做法是:
定义一个类
类名一般以
Error结尾继承自
Exception或其子类
例如:
class SchoolNameError(Exception):
def __init__(self, msg):
super().__init__('【校名异常】' + msg)2. 使用自定义异常进行业务校验
def check_school_name(name):
if len(name) > 10:
raise SchoolNameError('学校名过长')
else:
print('学校名是合法的')
try:
check_school_name('abcdefghijklmnop')
except SchoolNameError as e:
print(f'程序异常:{e}')3. 为什么要自定义异常
自定义异常的优势非常明显:
能够准确表达具体业务语义
便于分层捕获和分类处理
提升代码可读性与可维护性
更适合构建规范化的异常体系
例如,相较于简单抛出 ValueError:
raise ValueError('学校名过长')使用:
raise SchoolNameError('学校名过长')会让异常的来源、领域语义和处理方式更加清晰。
十四、异常处理的工程化建议
在实际项目开发中,异常处理不应停留在“能跑就行”的层面,而应尽量遵循以下原则:
1. 避免使用裸 except
裸 except 会捕获过于宽泛的异常,容易掩盖真实问题。
2. 优先捕获具体异常
针对 ZeroDivisionError、ValueError、KeyError 等分别处理,更有利于定位问题。
3. 保留异常上下文
必要时应记录异常对象、调用栈、输入参数等信息,方便后期排障。
4. 使用 finally 做资源清理
例如文件关闭、数据库连接释放、网络资源回收等。
5. 对业务错误使用自定义异常
复杂系统中,建议建立清晰的异常分类体系。
6. 不要滥用异常
异常机制用于处理“非正常流程”,不应替代普通的分支判断逻辑。
十五、总结
Python 的异常机制既是语言层面的基础能力,也是提升程序健壮性的重要保障。
本文梳理了以下核心内容:
错误 是语法层面的代码问题,必须通过修改代码解决
异常 是运行期间的问题,可以通过异常处理机制进行管理
try-except是最基础的异常捕获结构else用于处理成功路径,finally用于执行收尾动作raise可用于主动触发业务异常异常会沿调用链逐层传递,直到被捕获
自定义异常类可以让业务错误表达更加明确、专业、可维护
如果把 Python 程序比作一套运行中的系统,那么异常处理机制就是系统的“故障响应与恢复模块”。掌握它,不仅能让程序“避免崩溃”,更能让代码逐步具备 可诊断、可恢复、可维护 的工程化特征。