跳转至

第 5 章 · 异常

异常处理是 Python 的常规控制流,地位远比 Java 高。两个核心差异要先记住:Python 没有 checked exceptions(全是"unchecked"),且推崇 EAFP("先做,错了再说")而非 Java 习惯的 LBYL("先检查再做")。


5.1 异常体系与抛出

Python 所有异常继承自 BaseException,绝大多数你写的继承 Exception

  • Java

    throw new IllegalArgumentException("bad");
    
  • Python

    raise ValueError("bad")     # raise,不是 throw
    

常见内置异常对照:

Python Java 类比 何时抛
ValueError IllegalArgumentException 值的类型对但内容非法
TypeError 类型/操作不兼容(近似 IllegalArgumentException "a"+1、调用不可调用对象
KeyError (Map 找不到键) dict[key] 键不存在
IndexError IndexOutOfBoundsException 索引越界
FileNotFoundError FileNotFoundException 文件不存在
AttributeError (字段/方法不存在) 访问不存在的属性
ZeroDivisionError ArithmeticException 除以零

5.2 try/except:捕获

  • Java

    try {
        int n = Integer.parseInt(s);
    } catch (NumberFormatException e) {
        n = 0;
    } finally {
        cleanup();
    }
    
  • Python

    try:
        n = int(s)
    except ValueError:
        n = 0
    finally:
        cleanup()
    

要点:

  • except 不是 catch;可捕获多个类型:except (KeyError, IndexError):
  • as 绑定异常对象except ValueError as e: print(e)
  • except:(不写类型)捕获一切——强烈不推荐(连 KeyboardInterrupt 都吞掉,会导致 Ctrl+C 失效)。至少写 except Exception:
try:
    value = data["key"]
except KeyError as e:
    print(f"missing key: {e}")
    value = None

5.3 elsefinally:Python 比 Java 多一个子句

完整结构:

try:
    result = do_work()       # 可能出错
except ValueError:
    handle_error()           # 出错才走
else:
    use(result)              # ✅ 没出错才走(Python 独有)
finally:
    cleanup()                # 总是走

else 的意义:把"成功路径"从 try 里分离出来——这样 use(result) 里如果出错,不会被同层的 except ValueError 意外捕获。Java 没有这个子句,容易把成功代码塞进 try 导致误捕。

Pythonic 写法

else 就用它把成功路径和可能抛异常的代码分开,避免"过度捕获"。


5.4 没有 checked exceptions ⚠️

这是最大的思维转变:

  • Java

    // 必须声明或捕获 checked exception
    void load() throws IOException { ... }
    
  • Python

    def load() -> bytes:
        ...    # 没有声明,调用方"应该知道"可能出错
    

Python 的异常全是 unchecked(相当于全是 RuntimeException)——没有 throws 声明、没有编译期强制处理。

⚠️ Java 程序员的陷阱

你失去了"编译器提醒我这个函数可能抛哪些异常"的安全网。补救办法:

  • 靠文档字符串写明 Raises: 段落。
  • 靠类型注解:返回 T | None 或用 Raises 文档表达可能失败(部分团队用 Result 类型)。
  • 靠测试覆盖错误路径。

5.5 EAFP vs LBYL

两种风格:

  • LBYL(Java 习惯)

    # 先检查
    if "key" in d and isinstance(d["key"], int):
        process(d["key"])
    
  • EAFP(Python 习惯)

    # 直接做,错了再说
    try:
        process(d["key"])
    except (KeyError, TypeError):
        ...
    

EAFP = Easier to Ask Forgiveness than Permission。Python 社区倾向 EAFP,因为它:

  • 避免检查与使用之间的竞态(TOCTOU);
  • 代码更直白("做就完了");
  • 利用异常机制本身。

但也别走极端——EAFP 适合"异常确实罕见"的场景。如果失败是常态(如循环里大量 KeyError),用 dict.get() 之类避免性能损耗和滥用异常做控制流。

# 失败常见时,用 .get 而非 try/except
value = d.get("key", default_value)

5.6 重新抛出与异常链

重新抛出当前异常(裸 raise):

except ValueError:
    log.error("...")
    raise            # 不带参数 = 原样向上抛

异常链raise ... from)保留原始原因,调试时能看完整调用链:

try:
    data = json.loads(raw)
except json.JSONDecodeError as e:
    raise ValueError("invalid config") from e   # 链式:原始异常作为 __cause__

5.7 自定义异常

  • Java

    class InsufficientFundsException
        extends Exception { ... }   // checked
    
  • Python

    class InsufficientFundsError(Exception):
        """当账户余额不足时抛出。"""
        def __init__(self, balance: float, amount: float):
            super().__init__(f"need {amount}, only {balance}")
            self.balance = balance
            self.amount = amount
    

Pythonic 写法

  • 自定义异常继承 Exception(不要继承 BaseException,那是给 KeyboardInterrupt 这类系统级用的)。
  • 给异常起名带 Error 后缀(如 MyError),和标准库一致。
  • 设计异常层次AppError -> PaymentError -> InsufficientFundsError),便于上层按粒度捕获。

本章练习

练习 5.1

把下面 LBYL 代码改写成 EAFP,并说明 EAFP 版本如何避免竞态:

import os
if os.path.exists(path):
    with open(path) as f:
        return f.read()
return None

参考答案

try:
    with open(path) as f:
        return f.read()
except FileNotFoundError:
    return None
exists()open() 之间存在 TOCTOU 竞态(文件可能在检查后被删/创建)。EAFP 直接尝试,以"打开是否成功"为唯一真相,天然避免竞态。

练习 5.2

写一个自定义异常层次:AppError(Exception) -> ValidationError(AppError)。再写一个函数 parse_age(s),把字符串转成 int,若为负数抛 ValidationError,并保持原始异常链(若 int(s) 失败)。

参考答案
class AppError(Exception): ...
class ValidationError(AppError): ...

def parse_age(s: str) -> int:
    try:
        age = int(s)
    except ValueError as e:
        raise ValidationError(f"not an integer: {s!r}") from e
    if age < 0:
        raise ValidationError(f"age can't be negative: {age}")
    return age
练习 5.3

解释 except:except Exception: 的区别,以及为什么后者更安全。

参考答案

except: 捕获 BaseException 的所有子类,包括 KeyboardInterrupt(Ctrl+C)和 SystemExitsys.exit),会让程序无法正常中断/退出。except Exception: 只捕获普通异常,保留系统控制异常的传播,是安全的标准做法。


上一章:第 4 章 · OOP | 下一章:第 6 章 · 模块与包