一、错误 vs 异常
| 概念 | 说明 | 例子 |
|---|---|---|
| 语法错误 | 代码不符合 Python 语法规则,解释器无法解析,程序运行前就会报错 | if x = 1:(应为 ==)、缺少冒号 |
| 逻辑错误 | 语法正确但结果不符合预期,属于 bug,通常不会自动触发异常 | a / b 中 b 可能为 0 而未处理 |
| 异常 | 语法正确,但在运行期间发生错误,导致正常流程中断。Python 会抛出异常对象,可以被捕获处理 | ZeroDivisionError、TypeError、FileNotFoundError |
通常我们说的“异常处理”指的是对运行时异常的捕获与恢复,而不是修正语法错误。
二、异常处理基本结构:try / except
try:
# 可能抛出异常的代码
value = int(input("输入数字: "))
result = 10 / value
except ValueError:
print("请输入有效的整数")
except ZeroDivisionError:
print("不能除以零")
- 如果
try块中发生异常,Python 会跳转到匹配的第一个except块。 - 如果没有匹配的
except,异常会继续向上传递(可能导致程序崩溃)。
捕获所有异常(不推荐,除非日志后重新抛出)
try:
risky()
except Exception as e: # 捕获所有非系统退出的异常
print(f"出错了: {e}")
except:会捕获BaseException(包括KeyboardInterrupt、SystemExit),一般不要这样写。- 建议指定具体的异常类型或使用
except Exception as e。
三、else 和 finally
try:
file = open("data.txt")
data = file.read()
except FileNotFoundError:
print("文件不存在")
else:
# 仅在 try 块没有发生任何异常时执行
print(f"文件长度: {len(data)}")
finally:
# 无论是否发生异常,都会执行(通常用于清理资源)
if 'file' in locals():
file.close()
print("清理完毕")
| 子句 | 执行时机 |
|---|---|
else | 仅当 try 没有抛出异常时执行 |
finally | 无论是否发生异常都会执行,即使有 return 或 break |
四、手动抛出异常:raise
def withdraw(balance, amount):
if amount > balance:
raise ValueError(f"余额不足,需要 {amount},可用 {balance}")
return balance - amount
可以重新抛出捕获的异常:
try:
...
except ValueError as e:
print("记录日志")
raise # 原样抛出,保留调用栈
或抛出不同的异常(异常链):
try:
...
except IOError as e:
raise RuntimeError("读取配置失败") from e
五、自定义异常
通常继承自 Exception 或其子类。
class InsufficientFundsError(Exception):
"""账户余额不足时抛出"""
pass
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(f"余额不足,需要 {amount},现有 {self.balance}")
self.balance -= amount
六、常见内置异常
| 异常 | 触发场景 |
|---|---|
ZeroDivisionError | 除法或取模运算中除数为 0 |
TypeError | 操作或函数应用于不适当类型的对象(如 1 + "a") |
ValueError | 参数类型正确但值不合适(如 int("abc")) |
IndexError | 序列下标越界 |
KeyError | 字典中找不到键 |
AttributeError | 对象没有某属性 |
FileNotFoundError | 打开不存在的文件(Python 3 内置 OSError 子类) |
ImportError / ModuleNotFoundError | 导入模块失败 |
KeyboardInterrupt | 用户按 Ctrl+C 中断程序(继承自 BaseException,不是 Exception) |
StopIteration | 迭代器没有更多值(内部使用) |
七、异常传递与未捕获
如果异常一直向上传递到程序顶层(主线程)仍未被捕获,Python 会打印 traceback 并终止程序。
def a(): b()
def b(): c()
def c(): 1 / 0
a()
# Traceback (most recent call last):
# File "...", line ..., in <module>
# a()
# ...
# ZeroDivisionError: division by zero
八、assert 用于调试断言
assert condition, message 如果条件为假,抛出 AssertionError。通常在开发阶段用于检查不变量,生产环境可用 -O 选项忽略。
def divide(a, b):
assert b != 0, "除数不能为0"
return a / b
九、异常处理的最佳实践
- 捕获具体异常,不要使用裸露的
except:。 - 只捕获你能够处理的异常,否则让异常向上传播更合适。
- 清理资源使用
finally或上下文管理器(with语句)。 - 自定义异常使代码语义更清晰。
- 记录异常日志,同时可以考虑重新抛出(如果无法处理)。
- 不要用异常控制正常流程,异常处理开销较大。
十、示例:带重试的异常处理
import time
def fetch_data(url, retries=3):
for i in range(retries):
try:
response = requests.get(url, timeout=2)
response.raise_for_status() # 4xx/5xx 抛出 HTTPError
return response.json()
except (requests.ConnectionError, requests.Timeout) as e:
print(f"网络错误,重试 {i+1}/{retries}")
time.sleep(1)
except requests.HTTPError as e:
print(f"HTTP 错误 {e.response.status_code},不再重试")
raise # 不可恢复的错误,直接抛出
raise Exception(f"无法获取数据,已重试 {retries} 次")
总结
- 错误:语法错误无法运行;逻辑错误需要修改代码。
- 异常:运行时的意外情况,可以被捕获并恢复。
- 用
try/except捕获异常,else处理成功情况,finally执行清理。 - 用
raise主动抛出异常,可定义自己的异常类。 - 编写健壮的程序需要合理使用异常处理,但不要过度使用。