程序总会有出错的时候——用户输入了无效数据、网络连接中断、磁盘空间不足……优秀的程序员不是写出永不报错的代码(这是不可能的),而是能优雅地处理错误,让程序在异常情况下也能行为可预测、可排查。

本章将系统学习 Python 的异常处理机制——从 try/except 到自定义异常,再到使用 pdblogging 进行调试,让你的代码从"动不动就崩溃"变成"稳如磐石"。

Python 的异常哲学

Python 推崇 EAFP 原则——“Easier to Ask for Forgiveness than Permission”(请求原谅比获得许可更容易)。也就是:先尝试执行,如果出错了再处理,而不是在动手之前做各种检查。

# LBYL 风格(Look Before You Leap)——非 Python 方式
def safe_divide_lbyl(a, b):
    if b == 0:
        return None
    if not isinstance(a, (int, float)):
        return None
    return a / b

# EAFP 风格——Python 方式
def safe_divide_eafp(a, b):
    try:
        return a / b
    except (ZeroDivisionError, TypeError):
        return None

try/except/else/finally

Python 的异常处理结构由四个子句组成:

try:
    # 可能出错的代码
    result = risky_operation()
except SomeError:
    # 出错时执行的代码
    handle_error()
else:
    # 没出错时执行的代码
    process_result(result)
finally:
    # 无论是否出错,都执行的代码
    cleanup()

基础用法

try:
    number = int(input("请输入一个数字: "))
    result = 100 / number
    print(f"结果: {result}")
except ValueError:
    print("输入的不是有效数字!")
except ZeroDivisionError:
    print("不能除以零!")

else 子句

elsetry 块没有抛出异常时执行。它把"正常逻辑"和"错误处理"清晰分开:

try:
    data = load_file("config.json")
except FileNotFoundError:
    print("配置文件不存在,使用默认配置")
    data = DEFAULT_CONFIG
except json.JSONDecodeError:
    print("配置文件格式错误")
    data = DEFAULT_CONFIG
else:
    print(f"成功加载配置,共 {len(data)} 项")  # 只有没异常时才执行

finally 子句

finally 无论是否发生异常都会执行,通常用于资源清理

def read_file_safe(path):
    file = None
    try:
        file = open(path, "r", encoding="utf-8")
        return file.read()
    except FileNotFoundError:
        print(f"文件未找到: {path}")
        return ""
    finally:
        # 无论成败,都确保文件被关闭
        if file is not None:
            file.close()
            print("文件已关闭")

finally 的一个特殊行为——即使 try 中有 returnfinally 也会执行:

def demo():
    try:
        print("try 中")
        return "返回值"
    finally:
        print("finally —— 即使有 return 也会执行")

print(demo())
# try 中
# finally —— 即使有 return 也会执行
# 返回值

捕获特定异常

永远不要使用裸 except(不带异常类型)

# 极其不推荐——会捕获所有异常,包括 SystemExit、KeyboardInterrupt
try:
    result = risky_operation()
except:  # 裸 except
    print("出错了")
    result = None

# 正确——只捕获预期的异常
try:
    result = risky_operation()
except (ValueError, TypeError) as e:
    print(f"数据处理错误: {e}")
    result = None

获取异常信息

使用 as 关键字获取异常对象:

try:
    data = {"name": "Alice"}
    value = data["age"]
except KeyError as e:
    print(f"缺少键: {e}")          # 'age'
    print(f"异常类型: {type(e).__name__}")  # KeyError
    print(f"异常详情: {e.args}")   # ('age',)

多个异常的统一处理

# 方式 1:元组形式
try:
    process(data)
except (ValueError, TypeError, KeyError) as e:
    print(f"数据处理错误: {e}")

# 方式 2:链式 except(按优先级排列)
try:
    process(data)
except ValueError as e:
    print(f"值错误: {e}")
except TypeError as e:
    print(f"类型错误: {e}")
# 更具体的异常要放在前面!

异常层次结构

Python 的所有异常都继承自 BaseException。常见的继承链:

BaseException
├── SystemExit          # sys.exit() 触发
├── KeyboardInterrupt   # Ctrl+C 触发
└── Exception           # 所有常规异常的基类
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   └── OverflowError
    ├── LookupError
    │   ├── IndexError
    │   ├── KeyError
    │   └── ...
    ├── ValueError
    ├── TypeError
    ├── FileNotFoundError
    ├── AttributeError
    ├── ImportError
    │   └── ModuleNotFoundError
    └── ...

这意味着: 捕获 Exception 会捕获它所有的子类。捕获 ArithmeticError 会同时捕获 ZeroDivisionErrorOverflowError

try:
    result = 1 / 0
except ArithmeticError as e:
    # 也会捕获 ZeroDivisionError(它是 ArithmeticError 的子类)
    print(f"算术错误: {e}")

常见的内置异常

异常 含义 常见触发场景
ValueError 值不符合预期 int("abc")remove("不存在的元素")
TypeError 类型不匹配 "hello" + 42len(123)
IndexError 索引超出范围 [1,2,3][5]
KeyError 字典键不存在 {"a": 1}["b"]
FileNotFoundError 文件不存在 open("不存在的文件.txt")
ZeroDivisionError 除以零 1 / 0
AttributeError 属性不存在 "hello".nonexist()
ImportError 导入失败 import nonexistent_module
ModuleNotFoundError 模块未找到 ImportError 的子类
StopIteration 迭代结束 生成器耗尽时
AssertionError assert 失败 assert False
TimeoutError 操作超时 网络请求超时

raise —— 主动抛出异常

你可以在自己的代码中主动抛出异常,使用 raise 关键字:

def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("取款金额必须为正数")
    if amount > balance:
        raise ValueError("余额不足")
    return balance - amount

# 调用者负责处理
try:
    new_balance = withdraw(100, 200)
except ValueError as e:
    print(f"取款失败: {e}")

重新抛出异常

在捕获异常后,可以决定不处理而是继续向上抛出:

def process_order(order):
    try:
        save_to_database(order)
        send_confirmation_email(order)
    except DatabaseError:
        # 记录日志后重新抛出
        logger.error(f"数据库错误,订单 {order.id} 未保存")
        raise  # 重新抛出原始异常

注意: 使用裸 raise 会保留原始的 traceback。如果写成 raise e 则会丢失调用栈信息。

异常链(raise ... from ...

def load_config(path):
    try:
        with open(path) as f:
            return json.load(f)
    except FileNotFoundError as e:
        raise ConfigError(f"配置文件缺失: {path}") from e
    except json.JSONDecodeError as e:
        raise ConfigError(f"配置文件格式错误: {path}") from e

from e 会将原始异常附加到新的异常上,调试时可以看到完整的异常链条。

自定义异常类

对于大型项目,定义自己的异常层次结构是非常好的实践:

class AppError(Exception):
    """应用程序的基类异常"""
    pass

class ValidationError(AppError):
    """输入验证失败"""
    pass

class NotFoundError(AppError):
    """资源未找到"""
    pass

class AuthError(AppError):
    """认证或授权失败"""
    pass

class DatabaseError(AppError):
    """数据库操作失败"""
    pass

# 使用示例
def get_user(user_id):
    user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
    if not user:
        raise NotFoundError(f"用户 {user_id} 不存在")
    return user

def transfer(sender_id, receiver_id, amount):
    if amount <= 0:
        raise ValidationError("转账金额必须为正数")
    try:
        sender = get_user(sender_id)
        receiver = get_user(receiver_id)
    except NotFoundError as e:
        raise ValidationError("转账用户不存在") from e

自定义异常的命名通常以 Error 结尾,并继承自 Exception(而非 BaseException)。

assert 断言

assert 是 Python 的调试辅助工具,在开发阶段检查不应发生的条件:

def divide(a, b):
    assert b != 0, "除数不能为零"
    return a / b

# 等价于:
# if not (b != 0):
#     raise AssertionError("除数不能为零")
def calculate_average(numbers):
    assert len(numbers) > 0, "列表不能为空"
    assert all(isinstance(n, (int, float)) for n in numbers), "所有元素必须是数字"
    return sum(numbers) / len(numbers)

重要: assertpython -O(优化模式)下会被跳过。所以不要用 assert 来做数据验证或安全检查——这些应该用 if + 正常的异常处理。

# 不要这样做
def login(username, password):
    assert username, "用户名不能为空"  # 可能会被 -O 跳过!
    assert password, "密码不能为空"

# 应该这样做
def login(username, password):
    if not username:
        raise ValidationError("用户名不能为空")
    if not password:
        raise ValidationError("密码不能为空")

logging —— 替代 print 的调试方式

在调试时随手写 print() 很常见,但更好的方式是使用 logging 模块:

import logging

# 基本配置
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

# 获取日志器
logger = logging.getLogger(__name__)

# 不同级别的日志
logger.debug("调试信息——开发时使用")
logger.info("普通信息——确认程序运行正常")
logger.warning("警告——可能有问题的状况")
logger.error("错误——功能无法正常工作")
logger.critical("严重错误——程序可能崩溃")

配置日志输出到文件

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("app.log", encoding="utf-8"),  # 写入文件
        logging.StreamHandler(),                            # 同时输出到控制台
    ],
)

logger = logging.getLogger(__name__)
logger.info("程序启动")

logging vs print

对比项 print logging
级别控制 没有 DEBUG/INFO/WARNING/ERROR/CRITICAL
输出目标 只能控制台 文件、控制台、网络等
生产环境 需手动删除 调整 level 即可关闭调试日志
性能 始终执行 低于 level 的日志不会处理

建议: 从项目一开始就使用 logging,而不是 print

traceback 模块

traceback 模块可以获取和格式化异常的调用栈信息:

import traceback

try:
    1 / 0
except ZeroDivisionError:
    # 打印完整的调用栈
    traceback.print_exc()  # 等价于 print(traceback.format_exc())

    # 获取栈信息的字符串
    tb_str = traceback.format_exc()
    # 可以记录到日志或文件中
    with open("error.log", "a") as f:
        f.write(tb_str)
import traceback

def deep_func(n):
    if n <= 0:
        raise ValueError("递归终止")
    return deep_func(n - 1)

def handler():
    try:
        deep_func(5)
    except ValueError:
        # 获取精简的栈摘要
        stack_summary = traceback.extract_stack()
        for frame in stack_summary:
            print(f"  文件 {frame.filename}, 第 {frame.lineno} 行, 在 {frame.name}")

pdb —— Python 调试器

printlogging 不够用时,可以用 pdb(Python Debugger)进行交互式调试。

设置断点(Python 3.7+)

def calculate(x, y):
    result = x + y
    breakpoint()  # 在此处暂停,进入调试器
    result *= 2
    return result

print(calculate(3, 5))

breakpoint() 在 3.7+ 中是内置函数,等价于 import pdb; pdb.set_trace()

pdb 常用命令

命令 简写 作用
list l 显示当前行附近的代码
next n 执行下一行(不进入函数)
step s 进入当前行调用的函数
continue c 继续执行至下一个断点
print expr p 打印表达式值
pp expr 漂亮打印表达式
args a 打印当前函数的参数
return r 执行到当前函数返回
quit q 退出调试器

pdb 使用示例

# 示例:查找 bug
def process_data(data):
    total = 0
    for i, item in enumerate(data):
        breakpoint()  # 断点
        total += item["value"] * item["count"]
    return total

records = [
    {"value": 10, "count": 2},
    {"value": "5", "count": 3},  # 这里有问题!
    {"value": 20, "count": 1},
]

result = process_data(records)
print(result)

运行时的交互:

(Pdb) l  # 查看代码
(Pdb) p item  # 打印当前元素
(Pdb) p type(item["value"])  # 查看类型
(Pdb) n  # 下一步
(Pdb) c  # 继续执行

异常处理最佳实践

1. 精确捕获

# 不推荐——过于宽泛
try:
    result = process(data)
except Exception as e:
    print(f"出错了: {e}")

# 推荐——精确到具体异常
try:
    result = process(data)
except (ValueError, TypeError) as e:
    print(f"数据错误: {e}")
except ConnectionError as e:
    print(f"网络错误: {e}")
    retry()

2. 不要吞没异常

# 反模式——无声无息地吞没异常
try:
    result = risky_operation()
except Exception:
    pass  # 最糟糕的做法!

# 如果确实需要忽略,至少要记录日志
try:
    result = risky_operation()
except Exception as e:
    logger.warning(f"操作失败(可忽略): {e}")
    result = None

3. 在合适的层级处理

# 不好的设计——到处都在处理异常
def get_user_email(user_id):
    try:
        user = db.fetch_user(user_id)
        return user.email
    except Exception:  # 不应该在这里处理
        return None

# 更好的设计——让异常向上传播到合适的层级处理
def get_user_email(user_id):
    user = db.fetch_user(user_id)
    return user.email

# 在调用链的顶层集中处理
def handle_request(user_id):
    try:
        email = get_user_email(user_id)
        return {"email": email}
    except NotFoundError:
        return {"error": "用户不存在"}, 404
    except DatabaseError:
        return {"error": "服务暂时不可用"}, 500

4. 自定义异常的命名

class PaymentError(Exception):
    """支付相关的错误基类"""
    pass

class InsufficientFundsError(PaymentError):
    """余额不足"""
    pass

class CardDeclinedError(PaymentError):
    """卡片被拒绝"""
    pass

class PaymentTimeoutError(PaymentError):
    """支付超时"""
    pass

5. 异常处理 vs 条件检查

# 用异常处理比条件检查更 Pythonic
try:
    value = my_dict[key]
except KeyError:
    value = default

# 但有些场景条件检查更清晰
if not os.path.exists(path):
    create_directory(path)

完整示例:健壮的文件处理程序

综合运用本章的全部知识:

import json
import logging
from pathlib import Path

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)

class ConfigError(Exception):
    """配置相关错误"""
    pass

class DataProcessError(Exception):
    """数据处理错误"""
    pass

def load_config(path: Path) -> dict:
    """加载配置文件,处理常见的异常"""
    try:
        if not path.exists():
            raise FileNotFoundError(f"配置文件 {path} 不存在")

        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)

        if not isinstance(data, dict):
            raise ConfigError("配置文件必须是一个 JSON 对象")

        logger.info(f"成功加载配置: {path}")
        return data

    except FileNotFoundError as e:
        logger.warning(str(e))
        logger.info("使用默认配置")
        return {"theme": "light", "language": "zh"}
    except json.JSONDecodeError as e:
        raise ConfigError(f"JSON 解析错误: {e}") from e
    except PermissionError as e:
        raise ConfigError(f"权限不足: {e}") from e

def process_user_data(raw_data: str) -> dict:
    """处理用户数据,精确捕获每种异常"""
    if not raw_data or not raw_data.strip():
        raise DataProcessError("输入数据为空")

    try:
        user = json.loads(raw_data)
    except json.JSONDecodeError as e:
        raise DataProcessError(f"无效的 JSON 格式: {e}") from e

    required_fields = ["name", "age", "email"]
    missing = [f for f in required_fields if f not in user]
    if missing:
        raise DataProcessError(f"缺少必要字段: {', '.join(missing)}")

    try:
        user["age"] = int(user["age"])
    except (ValueError, TypeError) as e:
        raise DataProcessError(f"年龄必须是数字: {e}") from e

    if not isinstance(user["email"], str) or "@" not in user["email"]:
        raise DataProcessError("邮箱格式无效")

    return user

def main():
    """主函数——集中处理所有异常"""
    try:
        config = load_config(Path("config.json"))
        logger.info(f"配置: {config}")

        raw = '{"name": "Alice", "age": "28", "email": "alice@example.com"}'
        user = process_user_data(raw)
        logger.info(f"处理成功: {user['name']}, {user['age']}岁")

    except ConfigError as e:
        logger.critical(f"配置错误,程序无法启动: {e}")
    except DataProcessError as e:
        logger.error(f"数据处理失败: {e}")
    except Exception as e:
        logger.critical(f"未预期的错误: {e}", exc_info=True)

if __name__ == "__main__":
    main()

小结

本章我们学习了 Python 的异常处理和调试技术:

  • try/except/else/finally:完整的异常处理结构
  • EAFP 原则:“请求原谅比获得许可更容易”
  • 精确捕获异常:永远不要使用裸 except
  • 异常的层次结构:所有异常继承自 BaseException
  • raise 与异常链raise ... from e 保留错误上下文
  • 自定义异常:为项目创建自己的异常层次
  • assert:调试辅助,但不要用于数据验证
  • logging:用日志替代 print,可分级、可持久化
  • traceback:获取和记录调用栈信息
  • pdb:交互式调试器,breakpoint() 设置断点
  • 最佳实践:精确捕获、不吞没异常、在合适层级处理

下一步: 函数与模块篇到这里就结束了。接下来我们将进入面向对象篇,学习类与对象——Python 编程的核心范式。

Summary: 异常处理、logging、pdb 调试与自定义异常。