异常是程序运行时的错误信号。掌握异常处理的完整机制,能让代码在错误面前优雅降级、保留调试信息,而不是直接崩溃。
完整的异常语法块结构语法如下:
try:
# 可能抛出异常的逻辑
statements
except 异常类型1 as err1:
# 匹配异常1时执行
handle1
except (异常类型2, 异常类型3) as err2:
# 多种异常共用一套处理逻辑
handle2
except 异常类型4:
handle3
else:
# try 代码块没有发生任何异常才执行
else_statements
finally:
# 无论是否异常、是否return、是否break,必定执行
finally_statements例如,下面以除法运算为例:
def divide(a, b):
try:
result = a / b
except ZeroDivisionError as e:
print(f"[ERROR] 除零错误: {e}")
return None
else:
# 无异常时才会执行
print(f"[INFO] 计算成功: {result}")
return result
finally:
# 无论是否异常,都会执行(用于资源清理)
print("[INFO] 清理工作完成")
if __name__ == "__main__":
divide(10, 2) # 正常路径
divide(10, 0) # 异常路径运行代码,输出如下:
[INFO] 计算成功: 5.0
[INFO] 清理工作完成
[ERROR] 除零错误: division by zero
[INFO] 清理工作完成执行顺序:
try 块:执行业务逻辑
except:捕获指定异常时执行
else:无异常时才执行,适合放依赖成功结果的逻辑
finally:无论是否异常都执行,适合关闭文件、释放锁、断开连接
多异常分支语法如下:
try:
可能抛出异常的代码
except 异常类型1:
处理1
except 异常类型2:
处理2
except (异常3, 异常4):
统一处理多种异常
except Exception as e:
捕获其余所有常规异常
else:
try内无异常才执行
finally:
无论是否异常,一定会执行从上到下匹配,匹配到第一个符合的分支就不再往下走。例如:
import json
from typing import Any
def parse_config(raw: str) -> Any:
try:
obj = json.loads(raw)
port = obj["server"]["port"]
except json.JSONDecodeError as e:
print(f"[ERROR] JSON 解析失败: {e}")
return None
except KeyError as e:
print(f"[ERROR] 缺少必要字段: {e}")
return None
except TypeError as e:
print(f"[ERROR] 类型错误(期望字典,得到列表/字符串): {e}")
return None
else:
print(f"[INFO] 服务器端口: {port}")
return port
if __name__ == "__main__":
parse_config('{"server": {"port": 8080}}') # 正常
parse_config('不是 json') # JSONDecodeError
parse_config('{"server": 123}') # TypeError
parse_config('{"server": {"host": "0.0.0.0"}}') # KeyError运行代码,输出:
[INFO] 服务器端口: 8080
[ERROR] JSON 解析失败: Expecting value: line 1 column 1 (char 0)
[ERROR] 类型错误(期望字典,得到列表/字符串): 'int' object is not subscriptable
[ERROR] 缺少必要字段: 'port'注意异常捕获顺序原则为:先子类,后父类。如果 except Exception 写在最前面,后面的具体异常永远不会被触发。例如:
# 错误顺序示范(不要这样做)
try:
1 / 0
except Exception as e: # 吞掉了所有异常
print(f"通用错误: {e}")
except ZeroDivisionError as e: # 这行永远不会执行
print(f"除零错误: {e}")如果多个异常写在同一个 except(元组),则共用一套处理逻辑。例如:
def fetch_data():
try:
# 模拟网络或文件操作可能抛出多种异常
import random
exc = random.choice([IOError, ValueError, ConnectionError])
raise exc("模拟错误")
except (IOError, ConnectionError) as e:
print(f"[ERROR] IO/网络错误: {e}")
except ValueError as e:
print(f"[ERROR] 数据格式错误: {e}")
if __name__ == "__main__":
for _ in range(5):
fetch_data()运行代码,输出:
[ERROR] IO/网络错误: 模拟错误
[ERROR] IO/网络错误: 模拟错误
[ERROR] 数据格式错误: 模拟错误
[ERROR] IO/网络错误: 模拟错误
[ERROR] IO/网络错误: 模拟错误Python 内置的异常是有限的,不可能满足所有业务场景。此时,就需要我们自定义异常类。所有自定义异常必须继承自 Exception 或它的子类。
注意,不要直接继承 BaseException(会包含键盘中断、系统退出等非业务异常)。
自定义异常最简模板如下:
# 自定义业务异常
class BusinessError(Exception):
pass
# 使用自定义异常
raise BusinessError("业务执行失败")例如,自定义业务通用异常,然后继承业务通用异常自定义参数校验、权限不足和资源不存在。
class BusinessError(Exception):
"""业务通用异常"""
pass
class ValidationError(BusinessError):
"""参数校验失败"""
pass
class PermissionError(BusinessError):
"""权限不足"""
pass
class ResourceNotFoundError(BusinessError):
"""资源不存在"""
pass
# 使用自定义异常
def get_user(user_id: int):
if user_id <= 0:
# 抛出参数校验异常
raise ValidationError(f"user_id 必须大于 0,当前: {user_id}")
if user_id == 404:
# 抛出资源不存在异常
raise ResourceNotFoundError(f"用户 {user_id} 不存在")
if user_id == 403:
# 抛出无权访问异常
raise PermissionError(f"无权访问用户 {user_id}")
return {"id": user_id, "name": "Alice"}
if __name__ == "__main__":
for uid in [0, 404, 403, 1]:
try:
print(get_user(uid))
except BusinessError as e:
print(f"[业务错误] {type(e).__name__}: {e}")运行代码,输出:
[业务错误] ValidationError: user_id 必须大于 0,当前: 0
[业务错误] ResourceNotFoundError: 用户 404 不存在
[业务错误] PermissionError: 无权访问用户 403
{'id': 1, 'name': 'Alice'}上面自定义异常均不能传递参数,下面演示自定义异常允许传递参数信息,例如:
from typing import Any
class ApiError(Exception):
"""带 HTTP 状态码和业务代码的异常"""
def __init__(self, message: str, status_code: int = 500, code: str = "INTERNAL_ERROR"):
super().__init__(message)
self.status_code = status_code
self.code = code
self.message = message
def __repr__(self) -> str:
return f"ApiError(status_code={self.status_code}, code={self.code}, message={self.message!r})"
def to_dict(self) -> dict[str, Any]:
return {
"code": self.code,
"message": self.message,
"status_code": self.status_code,
}
class NotFoundError(ApiError):
def __init__(self, resource: str, resource_id: Any):
super().__init__(
message=f"{resource} {resource_id} 不存在",
status_code=404,
code="NOT_FOUND"
)
class UnauthorizedError(ApiError):
def __init__(self, detail: str = "身份验证失败"):
super().__init__(
message=detail,
status_code=401,
code="UNAUTHORIZED"
)
if __name__ == "__main__":
try:
raise NotFoundError("用户", 12345)
except ApiError as e:
print(f"异常: {e}")
print(f"字典: {e.to_dict()}")运行代码,输出:
异常: 用户 12345 不存在
字典: {'code': 'NOT_FOUND', 'message': '用户 12345 不存在', 'status_code': 404}异常设计建议:
自定义异常应该继承 Exception 或更具体的内置异常(如 ValueError)
不要直接继承 BaseException,否则会捕获 SystemExit 和 KeyboardInterrupt
提供 __init__ 接收上下文信息,方便上层记录日志或返回给客户端
raise 用于手动触发异常,中断正常流程,交由上层 try-except 捕获处理。例如:
from typing import Any
def transfer(from_id: int, to_id: int, amount: float) -> dict[str, Any]:
if amount <= 0:
raise ValueError("转账金额必须大于 0")
if from_id == to_id:
raise ValueError("不能给自己转账")
if amount > 1_000_000:
raise PermissionError("单笔转账超过限额,需要人工审批")
# 模拟业务逻辑
return {"from": from_id, "to": to_id, "amount": amount, "status": "success"}
if __name__ == "__main__":
for case in [(1, 2, 100), (1, 1, 100), (1, 2, -50), (1, 2, 2_000_000)]:
try:
result = transfer(*case)
print(f"[OK] {result}")
except (ValueError, PermissionError) as e:
print(f"[FAIL] {type(e).__name__}: {e}")运行代码,输出如下:
[OK] {'from': 1, 'to': 2, 'amount': 100, 'status': 'success'}
[FAIL] ValueError: 不能给自己转账
[FAIL] ValueError: 转账金额必须大于 0
[FAIL] PermissionError: 单笔转账超过限额,需要人工审批在封装底层异常时,保留原始堆栈信息对调试至关重要。例如,底层抛出了异常 A,被封装成异常 B,异常 B 被封装成异常 C,最终将异常 C 抛出。如果没有异常链,用户看见异常 C,根本不知道真实的异常信息和异常抛出地点。无法准确找到错误进行修复。
例如:配置未找到和配置格式错误,均通过 from e 将原始异常添加到异常链
import json
from typing import Any
class ConfigError(Exception):
"""配置加载失败"""
pass
def load_config(path: str) -> dict[str, Any]:
try:
with open(path, "r", encoding="utf-8") as f:
raw = f.read()
except FileNotFoundError as e:
# 用 raise ... from e 保留原始 FileNotFoundError 的堆栈
raise ConfigError(f"配置文件未找到: {path}") from e
try:
return json.loads(raw)
except json.JSONDecodeError as e:
raise ConfigError(f"配置文件 {path} JSON 格式错误,行 {e.lineno}, 列 {e.colno}") from e
if __name__ == "__main__":
# 情况 1:文件不存在
try:
load_config("/不存在的路径/config.json")
except ConfigError as e:
print(f"\n[错误] {e}")
print(f"[原始异常] {e.__cause__}")
# 情况 2:JSON 格式错误
import tempfile, os
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, encoding="utf-8") as f:
f.write("{ 不是合法 json")
tmp_path = f.name
try:
load_config(tmp_path)
except ConfigError as e:
print(f"\n[错误] {e}")
print(f"[原始异常] {type(e.__cause__).__name__}: {e.__cause__}")
finally:
os.unlink(tmp_path)运行代码,输出如下:
[错误] 配置文件未找到: /不存在的路径/config.json
[原始异常] [Errno 2] No such file or directory: '/不存在的路径/config.json'
[错误] 配置文件 C:\Users\ADMINI~1\AppData\Local\Temp\tmpzfk8362s.json JSON 格式错误,行 1, 列 3
[原始异常] JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 3 (char 2)异常链的好处:
上层调用方只关心 ConfigError 语义
调试时通过 __cause__ 可追溯到原始失败位置(文件不存在?JSON 格式错误?)
打印 traceback 时会自动显示 The above exception was the direct cause of the following exception:
在某些场景下,不希望暴露内部细节(如安全敏感信息),可以使用 from None 隐藏内部异常细节。
import hashlib
class AuthError(Exception):
"""认证失败(不暴露内部原因)"""
pass
def verify_password(stored_hash: str, password: str) -> None:
try:
# 假设某种复杂的密码验证逻辑
if stored_hash != hashlib.sha256(password.encode()).hexdigest():
raise ValueError("密码不匹配")
except Exception:
# 隐藏内部异常细节,防止攻击者推断系统逻辑
raise AuthError("用户名或密码错误") from None
if __name__ == "__main__":
try:
verify_password("abc123", "wrong")
except AuthError as e:
print(f"[认证失败] {e}")
# e.__cause__ 为 None,原始异常被隐藏
print(f"[原始异常] {e.__cause__}")运行代码,输出:
[认证失败] 用户名或密码错误
[原始异常] None注意:from None 只在安全敏感场景下使用。一般开发阶段建议保留完整链式信息。
assert 是开发调试工具,可被优化关闭,绝对不能用来做面向用户的业务校验。外部参数校验一律使用 if + raise 进行验证。
assert 语法如下:
assert 条件 [, 错误描述信息]等价于:
if not 条件:
raise AssertionError(错误描述)注意,断言可以全局关闭。在 Python 启动时增加 -O(大写字母 O,optimize)参数,所有 assert 语句会被直接删除,不再执行!
python -O main.py一旦开启优化模式:
assert age >= 0, "年龄不能负数"
# 这一行代码直接消失,判断彻底失效!例如:使用 assert 验证 width 和 height 参数是否大于 0。
class Rectangle:
def __init__(self, width: float, height: float):
assert width > 0, "width 必须为正数"
assert height > 0, "height 必须为正数"
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
if __name__ == "__main__":
r = Rectangle(3, 4)
print(f"面积: {r.area()}")
# 触发断言失败(运行 python -O 时会被优化掉,不会执行)
try:
Rectangle(-1, 4)
except AssertionError as e:
print(f"[断言失败] {e}")运行代码,输出:
面积: 12
[断言失败] width 必须为正数断言 vs 异常的选择:
| 场景 | 推荐方式 | 原因 |
| 内部程序员的假设(如私有方法) | assert | 发现 bug 时立即崩溃,开发阶段暴露问题 |
| 用户输入或外部数据校验 | raise ValueError | 断言在 python -O 下会被优化掉,不能用于生产校验 |
| 必须始终执行的校验 | if + raise | 即使优化模式也生效 |
裸 except 会捕获 SystemExit、KeyboardInterrupt,导致程序无法 Ctrl+C 退出。例如:
try:
do_something()
except: # 不要这样做!
pass
# 正确:至少捕获 Exception
try:
do_something()
except Exception as e:
logger.error("操作失败: %s", e)捕获太宽泛,吞掉了本不该处理的异常。例如:
def parse_int(s: str) -> int:
try:
return int(s)
except Exception: # 吞掉了 TypeError、MemoryError 等
return 0
# 正确:只捕获预期的异常
def parse_int(s: str) -> int | None:
try:
return int(s)
except ValueError:
return Nonefinally 中的异常会覆盖 try 中的异常,导致异常被覆盖,掩藏真实的异常信息。例如:
try:
1 / 0
finally:
raise RuntimeError("清理失败") # 原始的 ZeroDivisionError 被丢失!
# 正确:用 try/except 保护 finally 中的清理逻辑
try:
1 / 0
finally:
try:
cleanup()
except Exception as e:
logger.warning("清理失败: %s", e)将异常处理机制与日志记录能力结合,在程序运行过程中对各类异常情况进行完整捕获、合理处理的同时,同步完成关键事件、报错堆栈、上下文运行参数等信息的日志留存,便于后续问题定位、故障复盘以及运行状态分析。例如:
import logging
import traceback
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger(__name__)
class DatabaseError(Exception):
pass
def query_db(sql: str) -> list[dict]:
try:
# 模拟数据库连接
if "DROP" in sql.upper():
raise SyntaxError("检测到危险 SQL")
return [{"id": 1, "name": "Alice"}]
except SyntaxError as e:
logger.error("SQL 语法错误: %s", e)
raise DatabaseError("查询执行失败") from e
except Exception as e:
# 记录完整堆栈
logger.error("未预期异常:\n%s", traceback.format_exc())
raise
if __name__ == "__main__":
try:
query_db("SELECT * FROM users")
query_db("DROP TABLE users")
except DatabaseError as e:
logger.critical("业务层捕获: %s", e)运行代码,输出:
2026-08-13 15:43:40,064 [ERROR] SQL 语法错误: 检测到危险 SQL
2026-08-13 15:43:40,064 [CRITICAL] 业务层捕获: 查询执行失败