Python 异常、日志与调试:从踩坑到顺手的三件套

2026-05-26 26 预计阅读时间:1 分钟
来源:realpython.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

写 Python 代码,最怕的不是逻辑复杂,而是程序跑着跑着就"沉默崩溃"——没有报错信息,没有日志痕迹,只剩一个空荡荡的终端。异常处理、日志记录和调试手段,这三样东西单独看都不难,但真正在项目里用到位的人不多。下面把常见坑和实用模式串起来讲,每段都带可跑的代码。

异常:别只写 except Exception

新手最常见的写法:

try:
    do_something()
except Exception:
    pass  # 万能吞错误

这段代码的问题不是语法,而是把所有错误都吞掉了,后续排查时你连崩溃原因都找不到。正确做法是区分异常类型,只捕获你预期并能处理的:

import json

def load_config(path: str) -> dict:
    """加载 JSON 配置文件,只处理我们能恢复的错误。"""
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        # 配置缺失时返回默认值,这是可恢复的
        return {"debug": False, "port": 8080}
    except json.JSONDecodeError as e:
        # JSON 格式坏了,我们没法自动修,抛更明确的异常
        raise ConfigError(f"配置文件格式错误: {path}") from e

class ConfigError(Exception):
    """业务级配置异常,方便上层统一捕获。"""
    pass

几个要点:

  • 窄捕获:只 catch 你能处理的异常类型,其余让它往上冒泡。
  • 自定义异常:为业务领域定义异常类,调用方可以按业务语义 catch,而不是依赖底层异常类型。
  • from e 链式抛出:保留原始异常的 traceback,调试时能看到完整因果链。

日志:用 logging 替代 print

print 写调试信息很方便,但上线后你就得逐行删,或者被迫在代码里加 if DEBUG 判断。logging 模块一次配置,全局生效,还能按级别过滤。

一个够用的项目日志配置:

import logging
import sys

def setup_logging(level: str = "INFO", log_file: str = None):
    """一键配置日志,控制台 + 可选文件输出。"""
    root = logging.getLogger()
    root.setLevel(level.upper())

    formatter = logging.Formatter(
        fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )

    # 控制台输出
    console = logging.StreamHandler(sys.stdout)
    console.setFormatter(formatter)
    root.addHandler(console)

    # 可选文件输出
    if log_file:
        file_handler = logging.FileHandler(log_file, encoding="utf-8")
        file_handler.setFormatter(formatter)
        root.addHandler(file_handler)

# 使用示例
setup_logging(level="DEBUG", log_file="app.log")

logger = logging.getLogger("order_service")

def process_order(order_id: str):
    logger.debug("开始处理订单 %s", order_id)
    try:
        # ... 业务逻辑 ...
        logger.info("订单 %s 处理完成", order_id)
    except ConfigError as e:
        logger.error("订单 %s 处理失败: %s", order_id, e, exc_info=True)
        raise

关键细节:

  • %s 占位符而非 f-stringlogging 延迟格式化,如果这条日志被级别过滤掉,字符串拼接根本不会执行,性能差别在大量日志时很明显。
  • exc_info=True:在 logger.error 里加上这个参数,自动把完整 traceback 写进日志,不用手动 traceback.format_exc
  • 给 logger 取名字logging.getLogger("order_service") 让你后续可以单独调整这个模块的级别,比如生产环境只留 INFO,调试时单独把某个模块改成 DEBUG。

调试:pdb 三招够用

日志是事后看,调试是现场抓。Python 自带 pdb,不用装任何东西,三种触发方式:

1. 程序里埋断点

def calculate_discount(price: float, level: str) -> float:
    import pdb; pdb.set_trace()  # 运行到这里会暂停
    if level == "vip":
        return price * 0.8
    return price

2. 崩溃后事后复盘

python -m pdb my_script.py
# 程序崩溃后 pdb 自动停在异常位置
# 输入 l 看代码,输入 p var_name 看变量值

3. 交互式直接调试函数

python -i my_script.py
# 程序跑完进入交互模式,所有变量还在
# 然后手动 import pdb; pdb.pm() 进入事后调试

进入 pdb 后最常用的命令就这几个:

命令 作用
n 执行下一行
s 步入函数内部
c 继续运行到下一个断点
p x 打印变量 x 的值
l 显示当前代码上下文
where 显示调用栈

把三件套串起来:一个可跑的完整示例

下面这个小脚本演示了异常、日志、调试如何配合。你可以直接复制运行,改参数看不同行为:

#!/usr/bin/env python3
"""迷你订单处理脚本——异常 + 日志 + 调试配合演示。"""

import logging
import sys

# ---- 自定义异常 ----
class OrderError(Exception):
    pass

class InventoryError(OrderError):
    """库存不足。"""
    pass

# ---- 日志配置 ----
def setup_logging(level="INFO"):
    root = logging.getLogger()
    root.setLevel(level.upper())
    fmt = logging.Formatter(
        "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        datefmt="%H:%M:%S",
    )
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(fmt)
    root.addHandler(handler)

logger = logging.getLogger("order")

# ---- 业务逻辑 ----
INVENTORY = {"apple": 10, "banana": 3, "orange": 0}

def place_order(item: str, qty: int) -> str:
    logger.info("收到订单: item=%s qty=%d", item, qty)

    stock = INVENTORY.get(item, 0)
    if stock == 0:
        raise InventoryError(f"{item} 已售罄")
    if qty > stock:
        raise InventoryError(f"{item} 库存不足,剩余 {stock},请求 {qty}")

    INVENTORY[item] = stock - qty
    logger.info("订单完成: item=%s qty=%d 剩余=%d", item, qty, INVENTORY[item])
    return f"OK: {item} x{qty}"

def run_orders():
    """批量下单,单条失败不影响后续。"""
    orders = [("apple", 2), ("orange", 1), ("banana", 5), ("apple", 1)]
    for item, qty in orders:
        try:
            result = place_order(item, qty)
            print(result)
        except InventoryError as e:
            logger.warning("订单跳过: %s", e)
        except OrderError as e:
            logger.error("订单异常: %s", e, exc_info=True)

# ---- 入口 ----
if __name__ == "__main__":
    # 改成 DEBUG 可以看更多细节
    setup_logging(level="INFO")
    run_orders()
    print("\n最终库存:", INVENTORY)

运行结果:

08:30:01 [INFO] order: 收到订单: item=apple qty=2
08:30:01 [INFO] order: 订单完成: item=apple qty=2 剩余=8
OK: apple x2
08:30:01 [INFO] order: 收到订单: item=orange qty=1
08:30:01 [WARNING] order: 订单跳过: orange 已售罄
08:30:01 [INFO] order: 收到订单: item=banana qty=5
08:30:01 [WARNING] order: 订单跳过: banana 库存不足剩余 3请求 5
08:30:01 [INFO] order: 收到订单: item=apple qty=1
08:30:01 [INFO] order: 订单完成: item=apple qty=1 剩余=7
OK: apple x1

最终库存: {'apple': 7, 'banana': 3, 'orange': 0}

注意看:orange 售罄和 banana 库存不足都被 InventoryError 捕获,记录为 WARNING 后继续处理后续订单,而不是整个脚本崩溃退出。

上线前的检查清单

把这三样东西用到位,出问题时你能快速定位,不出问题时日志不会淹没磁盘。上线前过一遍:

  • 异常:每个 try/except 是否只捕获了预期类型?有没有裸 exceptexcept Exception: pass?自定义异常是否覆盖了核心业务场景?
  • 日志:是否用 logging 而非 print?日志格式是否包含时间、级别、模块名?生产级别是否设为 INFO 或 WARNING?文件日志是否配了轮转(RotatingFileHandler)?
  • 调试:代码里是否残留 pdb.set_trace()?是否还有临时 print 调试语句?关键路径的 logger.debug 是否足够,方便需要时切换级别查看?

这三件套不需要额外依赖,Python 自带就能覆盖大部分场景。习惯用好了,半夜被叫起来查问题时,你会感谢自己当初多写的那几行 logger.error 和自定义异常。


相关推荐