CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Python 语言基础/课时/异常处理
RawBlame
课时目录12 节

Python 语言基础

  1. 01Python 是什么
  2. 02语法与缩进
  3. 03类型与变量
  4. 04条件与循环
  5. 05函数
  6. 06列表字典与集合
  7. 07字符串处理
  8. 08文件读写
  9. 09模块与 pip
  10. 10类与对象入门
  11. 11异常处理
  12. 12综合练习与下一步

异常处理

try/except、raise。

25 分钟

异常处理

这一课你将能做到

  1. 用 try / except / else / finally 写出分层的异常处理,并精确捕获具体异常类型
  2. 用 raise 主动抛出异常、定义自己的异常类,让函数「出错时表达清楚」
  3. 面对报错 Traceback,能从下往上读出「异常类型、原因、出错行号」并定位问题

为什么要学

第 1 课你亲眼见过:Python 的错误要到运行那行才炸。程序只要和「外界」打交道——用户输入、文件、网络、数据库——就一定会遇到意外:文件不存在、网络超时、用户把年龄输成 "abc"。异常处理的本质是给意外预设好退路:出错可以,但要知道为什么错、错了以后程序往哪走。裸奔的脚本遇到一次异常就整体崩溃,前面的处理全部白费。

人话:try 是「这步可能有风险」,except 是「真出事了我的预案」,else 是「没出事才走的路」,finally 是「无论如何都要做的收尾」,raise 是「我认为这不对,我要拉响警报」。

先建立图像

try:                       ← 派出侦察兵
    危险操作()
except ValueError:         ← 预案 A:针对「值不对」
    处理 A()
except (KeyError, IndexError):  ← 预案 B:可以一次接多种
    处理 B()
else:                      ← 没触发任何预案时执行
    正常收尾()
finally:                   ← 无论走哪条路都执行
    必做的清理()

任何一层都没有预案 → 程序崩溃,打印 Traceback

Traceback 阅读法(从下往上读):

Traceback (most recent call last):   ← 最上面是「调用路径」
  File "app.py", line 5, in <module>
    main()                            ← 谁调用了谁
  File "app.py", line 2, in main
    int("abc")                        ← 一路追到案发现场
ValueError: invalid literal for int() with base 10: 'abc'
                                      ↑ 最后一行 = 异常类型 + 信息(最重要)

跟着做

步骤 1:捕获第一个异常

新建 exception_demo.py:

raw = input("请输入一个整数:")     # 故意输入 abc

try:
    n = int(raw)                   # 这行可能抛 ValueError
    print(f"你输入的是 {n}")
except ValueError:                 # 只接 ValueError
    print("这不是合法整数,请重新运行")

print("程序没有崩溃,继续往下走")

运行并输入 abc,输出:

请输入一个整数:abc
这不是合法整数,请重新运行
程序没有崩溃,继续往下走

对比不包 try 时直接崩溃——except 把异常「接住」了,程序回到主干道。

步骤 2:精确捕获多种异常

config = {"host": "localhost"}

def read_config(key):
    try:
        value = config[key]          # 可能 KeyError
        return int(value)            # 可能 ValueError
    except KeyError:
        print(f"配置项 {key} 不存在,使用默认值")
        return None
    except ValueError as e:          # as e:把异常对象接住看详情
        print(f"配置项 {key} 不是数字:{e}")
        return None

print(read_config("host"))           # localhost 不是数字 → None
print(read_config("port"))           # 配置项不存在 → None
print(read_config("timeout"))        # 演示前先补上它

输出:

配置项 host 不是数字:invalid literal for int() with base 10: 'localhost'
None
配置项 port 不存在,使用默认值
None

except 从上到下匹配,命中即走;把 int(value) 换成 float(value) 再试一次感受差异。一次接多种:except (KeyError, IndexError):。

步骤 3:else 与 finally 的执行时序

def safe_divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("除数不能为 0")
        return None
    else:
        # 只有 try 完全成功才进来(放「成功路径」逻辑)
        print(f"计算成功:{a} / {b} = {result}")
        return result
    finally:
        # 无论如何都执行(放清理逻辑)
        print("--- 本次计算结束 ---")

print(safe_divide(10, 2))
print(safe_divide(10, 0))

输出:

计算成功:10 / 2 = 5.0
--- 本次计算结束 ---
5.0
除数不能为 0
--- 本次计算结束 ---
None

执行顺序验证:失败路径也走了 finally,返回 None;成功路径 else → finally → return。

步骤 4:raise 主动抛出异常

def set_age(age):
    if not isinstance(age, int):
        raise TypeError(f"age 必须是 int,收到了 {type(age).__name__}")
    if not 0 <= age <= 150:
        raise ValueError(f"age 超出合理范围:{age}")
    print(f"年龄已设置为 {age}")

set_age(18)          # 正常
try:
    set_age(-5)      # 触发 ValueError
except ValueError as e:
    print("捕获到:", e)
try:
    set_age("18")    # 触发 TypeError
except TypeError as e:
    print("捕获到:", e)

输出:

年龄已设置为 18
捕获到: age 超出合理范围:-5
捕获到: age 必须是 int,收到了 str

raise 的信号意义:把「不合法」在错误现场立刻拦截,而不是返回 None 或 -1 让调用方猜。函数接口因此自文档化。

步骤 5:自定义异常类

class InsufficientBalanceError(Exception):     # 继承 Exception 即为自定义异常
    """余额不足异常"""
    def __init__(self, balance, amount):
        super().__init__(f"余额 {balance},无法支取 {amount}")
        self.balance = balance
        self.amount = amount

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientBalanceError(balance, amount)
    return balance - amount

try:
    withdraw(100, 500)
except InsufficientBalanceError as e:
    print(f"业务失败:{e}")
    print(f"当前余额仍是 {e.balance}")

输出:

业务失败:余额 100,无法支取 500
当前余额仍是 100

自定义异常的价值:调用方可以「按业务语义」精确捕获,而不是和 ValueError 这类通用异常混在一起。命名规范:以 Error 结尾。

步骤 6:异常链与「不要吞异常」

import json

def load_config(path):
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        raise RuntimeError(f"配置文件不存在:{path}") from None   # 链式说明见下

# 错误示范:裸 except 吞掉一切
def bad_load(path):
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except Exception:                # ⚠ 什么都接,连 KeyboardInterrupt 都想接
        return {}                    # 出错了也一声不吭 —— 排障黑洞

print(load_config("不存在的.json"))   # 会抛 RuntimeError,信息清晰

输出:

Traceback (most recent call last):
  File "exception_demo.py", line XX, in <module>
    print(load_config("不存在的.json"))
RuntimeError: 配置文件不存在:不存在的.json

三条铁律:①except 要写具体类型,不裸接 Exception;②接住后要么处理、要么记日志、要么重新 raise,别静默 return 默认值;③包装异常时 raise NewError(...) from e 保留原始现场(from None 表示刻意抹掉)。

讲清楚要点

  1. 异常是对象,层级是树:所有异常继承 BaseException,业务异常都在 Exception 分支下(ValueError、KeyError、FileNotFoundError…)。except 按继承关系匹配——接 Exception 会接住几乎一切,所以只能放在兜底位。
  2. try 块只包「可能出错的那几行」:把无关代码塞进 try 会扩大误捕范围,也让 else 语义失效。粒度最小化是专业习惯。
  3. except 顺序从具体到一般:except FileNotFoundError 要写在 except OSError 前面(前者是后者的子类),否则子类永远轮不到。
  4. else 放成功逻辑、finally 放清理:finally 无视 return、break 甚至异常,一定执行——所以 with 语句(文件关闭)本质上就是 try/finally 的语法糖。
  5. raise 三种姿势:raise 异常对象(最常用)、raise(在 except 里原样重抛)、raise 新异常 from 旧异常(包装并保留现场)。
  6. EAFP 风格:Python 惯用「先做再说,错了再接」(Easier to Ask Forgiveness than Permission),对比先 if 判断再做的 LBYL。try: d[k] except KeyError 比 if k in d 在并发/重复判断场景更干净。
  7. Traceback 是调试地图:最后一行是异常类型和信息;往上每个 File ... line ... 是一帧调用栈。定位顺序:先读最后一行,再跳到对应 line 号看代码。

你可能会懵的地方

  • except Exception 和裸 except 有什么区别? 裸 except: 连 SystemExit、KeyboardInterrupt(Ctrl+C)都接,基本等于「禁用错误系统」;except Exception: 至少放行系统级信号。但两者都会吞掉具体原因,正确做法是列出预期异常类型,Exception 只允许出现在最顶层兜底 + 记日志的场景。

  • 什么时候该捕获,什么时候让它崩? 能恢复的(用户输入错、可选文件缺失)→ 捕获处理;不能恢复的(配置全坏、代码 bug 如 AttributeError)→ 让它崩,崩溃信息本身就是最好的排障材料。「 catch 了但什么都不做」是最差的选择。

  • finally 里有 return 会怎样? finally 的 return 会覆盖 try/except 里的返回值,还会吞掉正在传播的异常。绝对不要在 finally 里写 return——想清理就用 with 或只做释放动作。

  • raise 的时候传字符串行不行? raise "出错了" 在 Python 3 直接 TypeError——raise 只接受异常类或异常实例。要么 raise ValueError("信息"),要么 raise ValueError(无信息版,会自动实例化)。

  • 我捕获了异常还想保留日志继续跑,怎么写? 用标准库 logging:except ValueError as e: logging.exception("处理失败") —— logging.exception 自动附上完整 Traceback。print(e) 只有一行信息,丢掉了调用栈。

错法对照

错法问题正解
裸 except:吞掉一切包括 Ctrl+C捕获具体异常类型
except Exception: pass静默吞错,排障黑洞记录日志或重新 raise
try 包住整大段代码误捕无关异常、定位困难try 只包危险行
用返回值 -1/None 表示出错调用方容易漏检查出错直接 raise 异常对象
finally 里写 return覆盖正常返回值、吞异常finally 只做清理
raise "错误信息"TypeError:必须是异常类raise ValueError("错误信息")
except 写在父类后面且被父类遮蔽子类分支永远走不到从具体到一般排列 except

卡点急救

  1. 程序崩了满屏 Traceback:从最后一行读起——异常类型 + 信息;再往上找第一个「你自己文件」的 line 号(系统库帧可以跳过)。
  2. 异常接住了但变量报 UnboundLocalError:try 里赋值失败,变量没建成。把变量的默认值放在 try 之前:result = None 再进 try。
  3. except 一直不命中:类型对不上。用 Traceback 最后一行的类型名去精确匹配;不确定就临时 except Exception as e: print(type(e)) 看真实类型(调试完删掉)。
  4. finally 意外执行了两次逻辑:把业务 return 混进了 finally。finally 只留清理语句(关连接、删临时文件)。
  5. 想看完整调用栈但被 except 吞了:在 except 里加 import traceback; traceback.print_exc(),或改用 logging.exception。

面试常问

  • try/except/else/finally 的完整执行顺序? try 正常 → else → finally;try 抛异常且被 except 接住 → except → finally;未被接住 → finally 执行完后异常继续向上传播。finally 在任何分支都执行,包括 return 之前。

  • Python 的 EAFP 和 LBYL 是什么? EAFP(先做,异常再处理):try: v = d[k] except KeyError;LBYL(先检查再做):if k in d: v = d[k]。Python 社区偏爱 EAFP,因为它少一次判断、且在竞态条件下更正确。这是面试考察「是否真写过 Python」的高频题。

  • raise 和 raise ... from ... 的区别? raise 单独使用在 except 块内表示原样重抛当前异常;raise NewError() from old 显式包装并保留异常链(__cause__),Traceback 会显示 "The above exception was the direct cause...";from None 刻意切断链。链式异常用于「底层错误翻译成高层语义」。

  • 自定义异常怎么写?为什么要自定义? 继承 Exception(或更贴切的内建异常)定义业务异常类,可携带额外字段(如余额、订单号)。价值:调用方可按业务语义精确 except;避免与通用异常混淆;日志与监控系统可按类型分类告警。

  • Java 受检异常 vs Python 异常模型的差异? Java 区分受检/非受检异常,受检异常强制声明或捕获;Python 无受检概念,任何异常都可以不被声明,靠文档和测试保证。Python 的观点是「异常处理是运行时决策」,代价是漏处理只能运行时暴露——所以更要靠测试覆盖。

自测三问

  1. try 里没有异常时,else 和 finally 的先后顺序?有异常且被接住时呢?
  2. 为什么 except FileNotFoundError 必须写在 except OSError 前面?
  3. 写出 InsufficientBalanceError 的定义(带 balance 字段)并在 withdraw 中 raise 它。

小结一句话

try/except 给意外留退路、raise 让错误在现场亮明身份、finally 保证收尾、Traceback 从下往上读——让程序「错得清楚、崩得体面」是脚本走向可靠的第一步。

延伸

  • 官方教程 — 错误与异常
  • 内置异常层级
  • 下一课预告:综合练习与下一步——把十二课串成一个真实小项目,并拿到数据/AI 路线的地图。
加载本节练习…
← 上一课类与对象入门下一课 →综合练习与下一步