调试不是「出 bug 才做的事」,而是理解程序行为的基本方法。本文从最快的
目录
- print 调试的艺术
- pdb 交互式调试
- 异常与 traceback 深入
- logging 架构
- 结构化日志 structlog
- 日志配置实战
- 性能剖析 py-spy 与 cProfile
- 内存泄漏定位
- 生产可观测性集成
- 速查表与最佳实践
1. print 调试的艺术
print 是最快的调试手段,但要 print 得聪明:
# ❌ 只打值,看不出上下文
print(x)
# ✅ 用 f-string 带变量名
print(f"{x=}") # Python 3.8+:输出 x=42
# ✅ 加位置标记
print(">>> 进入 parse_config, path =", path)
# ✅ 统一入口,一键关闭
DEBUG = os.environ.get("DEBUG") == "1"
def dbg(*args):
if DEBUG:
print(*args, file=sys.stderr) # 打到 stderr,不污染 stdout 管道
# ✅ 用 pprint 看嵌套结构
from pprint import pprint
pprint(complex_config, indent=2, width=80)
要点:生产代码的 print 应该全部清掉或转成 logging.debug,别让调试输出污染线上日志。
2. pdb 交互式调试
# 3.7+ 直接内建 breakpoint()
def process(data):
breakpoint() # 进入 pdb 断点
result = transform(data)
return result
pdb 常用命令:
| 命令 | 作用 |
|---|---|
n / next | 单步执行 |
s / step | 进入函数 |
c / continue | 运行到下一断点 |
l | 显示当前行上下文 |
p expr | 打印表达式 |
pp expr | 美观打印 |
u / d | 上/下调用栈 |
b file:line | 设置断点 |
w | 查看调用栈 |
r | 运行到函数返回 |
q | 退出 |
非交互式 / 事后调试:
# 崩溃后进入 pdb(事后分析)
python -m pdb my_script.py
# 只看结果不交互
python -c "import pdb; pdb.runcall(fn, *args)"
# ipdb 增强版(语法高亮 + tab 补全)
pip install ipdb
3. 异常与 traceback 深入
3.1 异常链
# 保留原始异常上下文
try:
raw = read_file("config.json")
except OSError as e:
raise ValueError("配置读取失败") from e # from 保留因果
# __context__ / __cause__
# raise ... from e → __cause__
# except 内 raise → __context__
3.2 完整 traceback
import traceback
# 打印完整堆栈
try:
1 / 0
except ZeroDivisionError:
traceback.print_exc() # 简洁版
traceback.print_exc(limit=3) # 限制帧数
tb = traceback.format_exc() # 拿到字符串(存日志)
# 获取当前调用栈
stack = traceback.extract_stack()
print(stack)
# 从 traceback 对象提取信息
exc_type, exc_value, exc_tb = sys.exc_info()
for frame in traceback.extract_tb(exc_tb):
print(frame.filename, frame.lineno, frame.name)
3.3 自定义异常
class AppError(Exception):
"""业务错误基类,附带机器可读 code"""
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
self.message = message
class NotFoundError(AppError):
pass
# 使用
def get_user(id):
user = db.find(id)
if user is None:
raise NotFoundError("USER_NOT_FOUND", f"用户 {id} 不存在")
return user
4. logging 架构
logging 的四大组件:Logger(入口)、Handler(输出)、Formatter(格式)、Filter(过滤)。
import logging
logger = logging.getLogger("myapp") # 分层命名 myapp.sub
# 基本用法
logger.debug("调试信息") # 默认不输出
logger.info("用户 %s 登录", user) # %s 延迟格式化,比 f-string 省开销
logger.warning("磁盘空间不足 %d%%", pct)
logger.error("处理失败", exc_info=True) # 带堆栈
logger.exception("处理失败") # 只在 except 里用,自动带堆栈
Level 优先级:DEBUG < INFO < WARNING < ERROR < CRITICAL
# 简单配置(开发)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
5. 结构化日志 structlog
文本日志难解析,生产用结构化日志(每行一个 JSON):
# pip install structlog
import structlog
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(), # 输出 JSON 行
]
)
log = structlog.get_logger()
# 上下文绑定(请求级)
structlog.contextvars.bind_contextvars(request_id="req-123", user_id=42)
log.info("支付成功", amount=99.9, gateway="alipay")
# → {"request_id": "req-123", "user_id": 42, "event": "支付成功", "amount": 99.9, "gateway": "alipay", "timestamp": "...", "level": "info"}
JSON 日志的价值:可被 Loki/ELK/Datadog 直接索引,grep、聚合、告警全部可用。
6. 日志配置实战
# logging_config.py
import logging
import logging.config
import json
def setup_logging(level: str = "INFO", json_logs: bool = False) -> None:
config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"text": {"format": "%(asctime)s %(levelname)s %(name)s %(message)s"},
"json": {"format": json.dumps({
"time": "%(asctime)s", "level": "%(levelname)s",
"logger": "%(name)s", "msg": "%(message)s"})},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "json" if json_logs else "text",
},
},
"root": {"level": level.upper(), "handlers": ["console"]},
"loggers": {
"uvicorn": {"level": "WARNING"}, # 降噪框架日志
"urllib3": {"level": "WARNING"},
},
}
logging.config.dictConfig(config)
文件轮转:
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler(
"app.log", maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5, # 保留 5 个
encoding="utf-8",
)
7. 性能剖析 py-spy 与 cProfile
7.1 cProfile(离线)
python -m cProfile -o prof.out my_script.py
python -m pstats prof.out
# 代码内剖析
import cProfile, pstats
prof = cProfile.Profile()
prof.enable()
# ... 被测代码 ...
prof.disable()
stats = pstats.Stats(prof)
stats.sort_stats("cumulative").print_stats(20) # 累计耗时 Top20
7.2 py-spy(线上不侵入)
pip install py-spy
# 附着到运行中的进程(无需重启、不改代码)
sudo py-spy top --pid 12345
sudo py-spy record --pid 12345 -o profile.svg --duration 30
区别:cProfile 需改代码且本身有开销;py-spy 用 ptrace 采样,可分析正在线上运行的进程,秒级定位 CPU 热点。
7.3 定位热点函数
# 手动打点测量
import time
def timed(fn):
import functools
@functools.wraps(fn)
def wrapper(*args, **kwargs):
t0 = time.perf_counter()
r = fn(*args, **kwargs)
print(f"{fn.__name__}: {(time.perf_counter()-t0)*1000:.2f}ms")
return r
return wrapper
8. 内存泄漏定位
import tracemalloc
# 启动追踪
tracemalloc.start()
# 快照对比
snap1 = tracemalloc.take_snapshot()
# ... 运行被测代码 ...
snap2 = tracemalloc.take_snapshot()
for stat in snap2.compare_to(snap1, "lineno")[:10]:
print(stat) # 显示新增分配最大的 10 行
# 查看当前最大占用
top = tracemalloc.take_snapshot().statistics("lineno")
for stat in top[:5]:
print(stat)
泄漏常见根因:全局缓存无限增长、循环引用未清理、对象被闭包/异常链持有。
# 配合弱引用
import weakref
cache = weakref.WeakValueDictionary() # 对象不存活就自动清除
9. 生产可观测性集成
| 场景 | 工具 | 集成方式 |
|---|---|---|
| 日志采集 | Loki / ELK | JSON 日志 → 采集 agent |
| 指标 | Prometheus | prometheus_client + Histogram |
| 链路追踪 | OpenTelemetry | opentelemetry-python 自动埋点 |
| 异常上报 | Sentry | sentry-sdk 一行接入 |
# Prometheus 指标示例
from prometheus_client import Histogram, Counter, start_http_server
request_dur = Histogram("http_request_duration_seconds", "HTTP 耗时",
buckets=[0.1, 0.5, 1, 2.5, 5, 10])
errors = Counter("http_errors_total", "错误数", ["status"])
start_http_server(8000)
def handle(req):
with request_dur.time():
try:
process(req)
except Exception:
errors.labels(status="500").inc()
raise
# Sentry
import sentry_sdk
sentry_sdk.init(dsn="...", traces_sample_rate=0.2)
# 自动捕获未处理异常 + 性能追踪
10. 速查表与最佳实践
| 需求 | 工具 |
|---|---|
| 快速看值 | print(f"{x=}") |
| 交互断点 | breakpoint() / ipdb |
| 事后分析 | python -m pdb script.py |
| 详细堆栈 | traceback.print_exc() |
| 规范日志 | logging + dictConfig |
| 结构化日志 | structlog + JSON |
| CPU 热点 | cProfile / py-spy |
| 内存泄漏 | tracemalloc / weakref |
| 线上可观测 | Prometheus + OTel + Sentry |
最佳实践:
- 调试用
print/breakpoint,但上线前清理或转成 logging。 - 生产日志结构化(JSON),别靠肉眼看文本。
logger.exception只在 except 块里用,自动带堆栈。- 用
%s占位而非 f-string,避免不必要格式化开销。 - 框架日志(uvicorn/urllib3)调高 level,降噪。
- 任何服务都要有指标 + 日志 + 追踪三件套。
一句话记忆:开发期靠 pdb + traceback 精准定位,生产期靠 JSON 日志 + 指标 + 追踪兜底;性能与内存问题分别用 py-spy 和 tracemalloc 下药。
延伸阅读
- Python 内存管理、垃圾回收与性能调优 —— 引用计数与 tracemalloc 深挖
- Python 测试与质量工程 —— 测试驱动防止回归
- Python 安全编程 —— 日志注入与敏感信息脱敏
- [[observability]] —— 可观测性工程三支柱
- [[devops]] —— SLO 错误预算与告警体系
调试能力 = 理解工具 + 理解程序状态。当你能「像看监控仪表一样」看代码运行时,几乎所有问题都能在一个小时内定位。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。