异常处理是 Python 开发中绕不开的环节。程序在运行阶段遇到非法输入、资源缺失或外部服务不可用时,解释器会抛出异常对象;如果未捕获,进程将带着 Traceback 终止。它与语法错误不同,语法错误在解释阶段就被发现,程序无法启动;而异常发生在运行时,可以通过代码去捕捉和管理。本文围绕内置异常类型、try/except/finally、raise、自定义异常、断言和上下文管理器展开,并给出实际建议。
Python 内置了丰富的异常类,它们都继承自 BaseException。常见的包括:ZeroDivisionError 表示除零操作;TypeError 表示操作或函数应用于不适当类型的对象;ValueError 表示参数类型正确但值不合法;IndexError 表示序列索引越界;KeyError 表示字典中查找不存在的键;FileNotFoundError 表示尝试打开不存在的文件;AttributeError 表示访问对象不存在的属性;ImportError 表示导入模块失败;NameError 表示访问未定义的变量;KeyboardInterrupt 表示用户中断执行,例如在终端按下 Ctrl+C。理解这些异常类型,有助于在编写 try/except 时准确匹配错误场景。
先看最简单的 try/except 基本形式:
- try:
- result = 10 / 0
- except ZeroDivisionError:
- print("错误:除数不能为零!")
复制代码
当 try 块中的代码触发 ZeroDivisionError 时,程序会跳转到对应的 except 分支执行,而不会直接崩溃。也可以在一个 try 后捕获多种异常,每种异常单独处理:
- try:
- num = int(input("请输入一个数字: "))
- result = 10 / num
- print(f"结果是: {result}")
- except ValueError:
- print("输入错误,请输入一个有效的数字。")
- except ZeroDivisionError:
- print("错误:除数不能为零!")
复制代码
使用 except Exception 可以捕获几乎所有异常,但代价是丢失具体错误类型,增加排查难度,因此不推荐在正式代码中笼统使用:
- try:
- risky_operation()
- except Exception as e:
- print(f"发生了一个错误: {e}")
复制代码
一个完整的异常处理结构由 try、except、else、finally 四部分组成。try 放置可能出错的代码,except 负责捕获并处理异常,else 在没有异常时执行,finally 无论是否发生异常都会执行,适合做资源清理。下面的例子演示了完整流程:
- def divide(a, b):
- try:
- result = a / b
- except ZeroDivisionError:
- print("除数不能为零!")
- return None
- except TypeError:
- print("参数类型错误,必须是数字。")
- return None
- else:
- print(f"计算成功!结果是: {result}")
- return result
- finally:
- print("除法运算执行完毕。")
- print(divide(10, 2))
- print(divide(10, 0))
- print(divide(10, 'a'))
复制代码
除了解释器自动抛出异常,开发者也可以使用 raise 语句主动触发异常,常用于参数检查或业务规则校验。例如设置年龄时,先检查类型再检查取值范围:
- def set_age(age):
- if not isinstance(age, int):
- raise TypeError("年龄必须是整数")
- if age < 0 or age > 150:
- raise ValueError("年龄必须在 0 到 150 之间")
- print(f"年龄设置为: {age}")
- try:
- set_age(-5)
- except ValueError as e:
- print(f"输入无效: {e}")
复制代码
当内置异常无法准确表达业务语义时,可以继承 Exception 创建自定义异常类。自定义异常能携带更多上下文信息,让调用方快速定位问题。例如银行账户余额不足的场景:
- class InsufficientFundsError(Exception):
- """余额不足异常"""
- def __init__(self, balance, amount):
- self.balance = balance
- self.amount = amount
- message = f"余额不足。当前余额: {balance}, 尝试提取: {amount}"
- super().__init__(message)
- class BankAccount:
- def __init__(self, balance):
- self.balance = balance
- def withdraw(self, amount):
- if amount > self.balance:
- raise InsufficientFundsError(self.balance, amount)
- self.balance -= amount
- print(f"取款 {amount} 成功。剩余余额: {self.balance}")
- account = BankAccount(100)
- try:
- account.withdraw(200)
- except InsufficientFundsError as e:
- print(e)
复制代码
在 Python 3 中,使用 raise ... from ... 可以将底层异常与上层异常关联起来,形成异常链。这样既能向上层抛出更具业务含义的异常,也能保留原始错误信息,便于追踪根本原因:
- def read_config(filepath):
- try:
- with open(filepath, 'r') as f:
- return f.read()
- except FileNotFoundError as e:
- raise RuntimeError(f"无法读取配置文件: {filepath}") from e
- try:
- config = read_config('config.ini')
- except RuntimeError as e:
- print(f"程序启动失败: {e}")
- print(f"根本原因: {e.__cause__}")
复制代码
assert 语句是开发阶段常用的调试手段,当条件为 False 时触发 AssertionError。需要注意,assert 可以通过 Python 的 -O 参数全局禁用,因此不能用于生产环境的核心数据校验。例如检查折扣率是否在合法区间:
- def calculate_discount(price, discount_rate):
- assert 0 <= discount_rate <= 1, "折扣率必须在 0 到 1 之间"
- return price * (1 - discount_rate)
- print(calculate_discount(100, 0.2))
- print(calculate_discount(100, 1.5)) # 触发 AssertionError
复制代码
with 语句与上下文管理器用于优雅地管理资源。文件操作是最经典的用例,即使 with 块内发生异常,文件也会被正确关闭:
- try:
- with open('data.txt', 'r') as file:
- content = file.read()
- except FileNotFoundError:
- print("文件不存在")
复制代码
还可以通过实现 __enter__ 和 __exit__ 方法自定义上下文管理器。下面的 Timer 类可以统计代码块执行耗时,__exit__ 返回 False 表示异常不会被抑制:
- class Timer:
- def __enter__(self):
- import time
- self.start = time.time()
- return self
- def __exit__(self, exc_type, exc_val, exc_tb):
- import time
- self.end = time.time()
- print(f"代码块执行耗时: {self.end - self.start:.2f} 秒")
- return False
- with Timer():
- sum(i for i in range(1000000))
复制代码
异常处理的最佳实践可以归纳为以下几点:尽可能捕获具体异常类型,避免使用笼统的 except Exception;不要静默吞掉异常,至少记录日志;保持 try 块精简,只包裹真正可能出错的代码;使用 else 将正常逻辑与异常逻辑分离;使用 finally 清理文件、网络连接等资源;自定义异常时,类名和错误信息要有明确业务含义;最后,区分异常与普通错误,简单的条件判断能处理的场景不必使用异常机制。
异常处理的最终目的不是掩盖错误,而是以一种可控、可预期的方式管理错误,提升程序的稳定性和用户体验。掌握从基础 try/except 到自定义异常、上下文管理器的用法,是编写健壮 Python 程序的重要技能。 |