这篇文章带你系统掌握 Python 异常处理——try-except-finally、常见异常类型速查、自定义异常,以及把第 07 篇的 JSON 操作升级为生产级别的健壮代码。
前言
写代码最怕什么?程序在用户手上崩溃。
前端开发者对 try-catch 一定不陌生——网络请求失败、JSON 解析出错、DOM 元素不存在……这些都需要异常处理来兜底。
Python 里的异常处理和 JS 高度相似,但有一些重要的扩展能力:比 JS 更精细的异常分类、else 子句、自定义异常类。
这篇文章覆盖:
- try-except-finally 基本用法(对比 JS try-catch-finally)
- 实战:给第 07 篇的 JSON 文件操作加上完整异常处理
一、为什么需要异常处理?
先看一个没有异常处理的代码:
import jsondef read_config(filepath): with open(filepath, "r") as f: return json.load(f)config = read_config("config.json")print(config["database"]["host"])
当任何一步出问题时,程序直接崩溃并抛出错误信息——文件不存在、JSON 格式错误、键不存在……用户看到的是一堆红色报错,体验极差。
加上异常处理后,程序可以优雅地处理这些情况,给出友好提示,甚至自动恢复。
二、基本语法:try-except
2.1 最简单的用法
try: result = 10 / 0 # 这行会抛出 ZeroDivisionError print("不会执行到这里")except ZeroDivisionError: print("❌ 除数不能为零")print("程序继续运行")
对比 JS:
try { const result = riskyOperation()} catch (error) { console.error("出错了:", error.message)}
核心区别:
- Python 的 except 可以指定异常类型(精准捕获)
- JS 的 catch 捕获所有异常,需要在内部判断类型
2.2 捕获多种异常
def parse_number(text): try: number = int(text) result = 100 / number return result except ValueError: print(f"❌ '{text}' 不是有效的数字") except ZeroDivisionError: print("❌ 不能除以零") except Exception as e: # 兜底:捕获所有其他异常 print(f"❌ 未知错误:{e}")parse_number("abc") # 触发 ValueErrorparse_number("0") # 触发 ZeroDivisionErrorparse_number("5") # 正常运行,返回 20.0
# 也可以用元组一次捕获多种异常try: ...except (ValueError, TypeError) as e: print(f"输入类型错误:{e}")
2.3 获取异常信息
try: with open("不存在的文件.txt") as f: content = f.read()except FileNotFoundError as e: print(f"错误类型:{type(e).__name__}") # FileNotFoundError print(f"错误信息:{e}") # [Errno 2] No such file or directory print(f"错误码:{e.errno}") # 2
三、完整结构:try-except-else-finally
Python 的异常处理有四个子句,比 JS 多了一个 else:
try: # 尝试执行的代码 result = int("42")except ValueError as e: # 只有 try 块发生异常时执行 print(f"转换失败:{e}")else: # 只有 try 块没有异常时执行(成功时的逻辑) print(f"转换成功:{result}")finally: # 无论是否异常,总会执行(用于清理资源) print("执行结束")
else 的用途: 将"成功时的逻辑"和"主要代码"分开,代码结构更清晰。
# 实际场景:文件操作try: with open("data.json", "r") as f: data = json.load(f)except FileNotFoundError: print("配置文件不存在,使用默认配置") data = {}except json.JSONDecodeError: print("配置文件格式错误") data = {}else: print(f"✅ 成功读取 {len(data)} 个配置项")finally: print("配置加载流程结束")
对比 JS:
try { const data = JSON.parse(text) console.log("✅ 解析成功") // else 的功能在 try 里实现} catch (e) { console.error("解析失败", e)} finally { console.log("结束")}
四、常见内置异常速查
| | |
|---|
ValueError | | int("abc") |
TypeError | | "1" + 1 |
KeyError | | d["不存在的键"] |
IndexError | | lst[100] |
FileNotFoundError | | open("不存在.txt") |
PermissionError | | |
AttributeError | | None.split() |
ZeroDivisionError | | 10 / 0 |
ImportError | | import 不存在的库 |
NameError | | print(未定义的变量) |
json.JSONDecodeError | | json.loads("{broken}") |
StopIteration | | next(空迭代器) |
RecursionError | | |
异常继承关系(部分):
BaseException └── Exception ├── ValueError ├── TypeError ├── OSError │ ├── FileNotFoundError │ └── PermissionError ├── LookupError │ ├── KeyError │ └── IndexError └── ArithmeticError └── ZeroDivisionError
理解继承关系很重要:捕获 OSError 就能同时捕获 FileNotFoundError 和 PermissionError。捕获 Exception 可以捕获几乎所有异常(但不推荐,太宽泛了)。
五、主动抛出异常:raise
有时候需要主动让程序抛出异常,用 raise:
def set_age(age): if not isinstance(age, int): raise TypeError(f"age 必须是整数,收到的是 {type(age).__name__}") if age < 0 or age > 150: raise ValueError(f"age 的合法范围是 0-150,收到的是 {age}") return agetry: set_age("二十八") # 触发 TypeErrorexcept TypeError as e: print(f"类型错误:{e}")try: set_age(200) # 触发 ValueErrorexcept ValueError as e: print(f"值错误:{e}")
在 except 中重新抛出:
def process_file(filepath): try: with open(filepath) as f: data = f.read() except FileNotFoundError: print(f"日志:文件 {filepath} 不存在") raise # 重新抛出同一个异常,让上层调用者处理
六、自定义异常类
对于业务逻辑中的错误,建议创建自定义异常类,让错误信息更有语义:
# 定义自定义异常class ConfigError(Exception): """配置相关错误""" passclass DatabaseError(Exception): """数据库相关错误""" def __init__(self, message, error_code=None): super().__init__(message) self.error_code = error_codeclass UserNotFoundError(Exception): """用户不存在""" def __init__(self, user_id): super().__init__(f"用户 ID {user_id} 不存在") self.user_id = user_id
# 使用自定义异常def get_user(user_id): users = {1: "Alice", 2: "Bob"} if user_id not in users: raise UserNotFoundError(user_id) return users[user_id]try: user = get_user(999)except UserNotFoundError as e: print(f"错误:{e}") # 用户 ID 999 不存在 print(f"查询的 ID:{e.user_id}") # 999
自定义异常的好处:
- 上层可以精准捕获特定业务错误,而不是捕获宽泛的 Exception
- 可以携带额外的错误上下文信息(如 user_id、error_code)
七、实战:给 JSON 文件操作加上完整异常处理
延续第 07 篇的学生成绩分析案例,现在我们加上完整的异常处理,让代码达到生产可用的标准:

import jsonfrom pathlib import Path# 自定义异常class DataFileError(Exception): """数据文件相关错误""" passdef read_students_data(filepath: str) -> dict: """ 读取学生数据 JSON 文件 Returns: dict: 学生数据 Raises: DataFileError: 文件不存在、权限不足或格式错误时 """ path = Path(filepath) # 检查文件是否存在 if not path.exists(): raise DataFileError(f"数据文件不存在:{filepath}") # 检查文件扩展名 if path.suffix.lower() != ".json": raise DataFileError(f"文件格式不正确,需要 .json 文件,收到:{path.suffix}") try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) except PermissionError: raise DataFileError(f"没有读取文件的权限:{filepath}") except json.JSONDecodeError as e: raise DataFileError(f"JSON 格式错误(第 {e.lineno} 行):{e.msg}") # 验证数据结构 if "students" not in data: raise DataFileError("数据文件缺少 'students' 字段") return datadef save_report(report: dict, filepath: str) -> None: """ 保存分析报告 Raises: DataFileError: 写入失败时 """ path = Path(filepath) try: # 确保输出目录存在 path.parent.mkdir(parents=True, exist_ok=True) with open(path, "w", encoding="utf-8") as f: json.dump(report, f, indent=2, ensure_ascii=False) except PermissionError: raise DataFileError(f"没有写入文件的权限:{filepath}") except OSError as e: raise DataFileError(f"写入文件失败:{e}")def calculate_average(scores: dict) -> float: """计算平均分,处理空字典的情况""" if not scores: return 0.0 return sum(scores.values()) / len(scores)def analyze_students(input_file: str, output_file: str) -> bool: """ 分析学生成绩 Returns: bool: 成功返回 True,失败返回 False """ try: # 读取数据 data = read_students_data(input_file) students = data["students"] if not students: print("⚠️ 数据文件中没有学生记录") return False # 处理成绩 results = [] for student in students: # 防止 scores 字段缺失 scores = student.get("scores", {}) avg = calculate_average(scores) results.append({ "name": student.get("name", "未知"), "scores": scores, "average": round(avg, 2) }) # 排序 results.sort(key=lambda s: s["average"], reverse=True) # 构建报告 report = { "class": data.get("class", "未命名班级"), "total_students": len(results), "top_student": results[0]["name"], "top_average": results[0]["average"], "rankings": results } # 保存结果 save_report(report, output_file) print(f"✅ 分析完成!结果已保存到 {output_file}") print(f"🏆 最高分:{results[0]['name']}(平均 {results[0]['average']} 分)") return True except DataFileError as e: # 业务层错误:给用户友好提示 print(f"❌ 数据处理失败:{e}") return False except Exception as e: # 未预期错误:记录并报告 print(f"❌ 发生未知错误:{type(e).__name__}: {e}") return False# 主程序if __name__ == "__main__": success = analyze_students("students.json", "output/report.json") if not success: print("请检查输入文件后重试")
测试各种异常情况:
# 测试文件不存在analyze_students("不存在的文件.json", "output/report.json")# ❌ 数据处理失败:数据文件不存在:不存在的文件.json# 测试 JSON 格式错误analyze_students("broken.json", "output/report.json")# ❌ 数据处理失败:JSON 格式错误(第 3 行):Expecting ',' delimiter# 测试正常情况analyze_students("students.json", "output/report.json")# ✅ 分析完成!结果已保存到 output/report.json# 🏆 最高分:Alice(平均 91.67 分)
八、异常处理的最佳实践
✅ 应该做的:
# 1. 精准捕获具体异常类型,而不是一刀切捕获 Exceptionexcept FileNotFoundError: # ✅ 好except Exception: # ⚠️ 太宽泛,慎用# 2. 捕获异常后给出有意义的错误信息except ValueError as e: print(f"输入值无效:{e}") # ✅ 有上下文# 3. 用 finally 确保资源释放(虽然 with 已经处理了文件)try: conn = get_db_connection() # 操作数据库finally: conn.close() # ✅ 确保连接关闭# 4. 只捕获你知道如何处理的异常,其他的让它向上传播
❌ 避免的写法:
# 1. 沉默所有异常(最危险)try: do_something()except: # ❌ 没有指定类型,连 KeyboardInterrupt 都会被吞掉 pass# 2. 捕获了但什么都不做(bug 会被隐藏)try: result = process(data)except Exception: pass # ❌ 发生错误了但程序继续,后续可能产生难以排查的 bug# 3. 用异常控制正常流程(性能差,可读性低)try: value = my_dict["key"]except KeyError: value = "default"# ✅ 改用:value = my_dict.get("key", "default")
小结
| |
|---|
| try-except-else-finally |
| except ValueError |
| except ValueError as e |
| raise ValueError("说明原因") |
| 继承 Exception,为业务错误提供语义化类型 |
| except |
3 个核心原则:
- 捕获具体异常类型,不要一刀切 except Exception
- 用 finally 或 with 确保资源(文件、连接)一定被释放
第一阶段收官 🎉
恭喜!第 01-08 篇的 Python 基础入门系列就此完结。
你现在掌握了:
这些已经足以让你写出实用的 Python 脚本。从下一篇开始,我们进入第二阶段:Python 进阶实用,第一个话题是——面向对象编程,用前端组件思维来理解 class。
下篇预告
第 09 篇:Python 面向对象编程:用前端组件思维来理解 Class
类 = 组件模板,实例 = 组件实例。如果你理解 React 组件,那 Python 的 OOP 你一定能快速上手。下一篇见!