写 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-string:logging延迟格式化,如果这条日志被级别过滤掉,字符串拼接根本不会执行,性能差别在大量日志时很明显。 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是否只捕获了预期类型?有没有裸except或except Exception: pass?自定义异常是否覆盖了核心业务场景? - 日志:是否用
logging而非print?日志格式是否包含时间、级别、模块名?生产级别是否设为 INFO 或 WARNING?文件日志是否配了轮转(RotatingFileHandler)? - 调试:代码里是否残留
pdb.set_trace()?是否还有临时print调试语句?关键路径的logger.debug是否足够,方便需要时切换级别查看?
这三件套不需要额外依赖,Python 自带就能覆盖大部分场景。习惯用好了,半夜被叫起来查问题时,你会感谢自己当初多写的那几行 logger.error 和自定义异常。