📖 知识点简介
代码写出来不叫写完了,调通了才算 。今天的核心是让你掌握一套调试方法论:从最原始的 print 打印、到 assert 断言的防御式检查、再到 logging 的日志体系,最后是 pdb/ipdb 的交互式断点调试。学会这些,90% 的 Bug 你都能亲手解决。
1️⃣ print 调试 --- 最简单也最容易被低估
print 调试的"原罪"是会忘记删。但作为一个快速验证手段,它依然好用。
带标记的 print
python
# ❌ 错误的 print 调试
def process(data):
result = do_something(data)
print(result) # 打印完你根本不知道这个值是哪来的
return result
# ✅ 带标记的 print
def process(data):
result = do_something(data)
print(f"[DEBUG] process.result = {result!r}") # !r 会显示 repr
return result
!r 和 pprint 让你看清数据结构
python
from pprint import pprint
data = {
"users": [
{"name": "Alice", "scores": [85, 92, 78]},
{"name": "Bob", "scores": [None, 88, 95]}, # None 值!
]
}
# print 糊成一团
print(data)
# pprint 清晰展示嵌套结构
pprint(data, indent=2, width=40)
# 输出:
# { 'users': [ { 'name': 'Alice',
# 'scores': [85, 92, 78]},
# { 'name': 'Bob',
# 'scores': [None, 88, 95]}]}
2️⃣ assert --- 防御式编程利器
assert 是写在代码里的"安全检查"------条件不满足就立刻抛出 AssertionError,阻止错误数据继续传播。
基本用法
python
def divide(a: int, b: int) -> float:
assert b != 0, "除数不能为零!"
assert isinstance(a, (int, float)), f"a 必须是数字,收到 {type(a)}"
return a / b
实战场景
python
def process_batch(items: list[dict]) -> list[dict]:
"""批量处理数据,用 assert 做防御式检查"""
assert isinstance(items, list), f"需要 list,收到 {type(items)}"
assert len(items) > 0, "列表不能为空"
results = []
for item in items:
# 断言每个元素的结构
assert "id" in item, f"缺少 id 字段: {item}"
assert "value" in item, f"缺少 value 字段: {item}"
processed = transform(item)
results.append(processed)
assert len(results) == len(items), "结果数量不一致"
return results
🚨 避坑:别过度使用 assert
python
# ❌ 不应该用 assert:
# 1. 关键业务逻辑(生产环境可能禁用 assert)
assert is_admin(user), "不是管理员" # 危险!生产环境可能不报错
# 2. 用户输入校验
assert len(password) > 8, "密码太短" # 应该用 if + 抛出 ValueError
# 3. 有副作用的表达式
assert db.save(user) # 关闭优化时不会执行 db.save()
# ✅ assert 的正确场景:
# - 内部不变式检查(这个值应该始终大于 0)
# - 函数前后条件断言(前置条件/后置条件)
# - 开发和测试时的帮助信息
重要 :
python -O启动时全部assert会被跳过!别把 assert 当安全检查用。
3️⃣ logging --- 把 print 升级为专业日志
print 的命运是"用完后删掉,下次又要写"。logging 是它的工业级替代品。
快速上手
python
import logging
# 基本配置(只需一次,通常在程序入口做)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%H:%M:%S",
)
# 获取 logger(建议每个模块一个)
logger = logging.getLogger(__name__)
# 不同级别
logger.debug("这是调试信息,开发时看")
logger.info("程序正常运行信息")
logger.warning("警告,可能有问题")
logger.error("发生了错误,但程序还能继续")
logger.critical("严重错误,程序可能崩了")
实际对比
python
# ❌ print 调试删不干净版
def fetch_data(url):
print(f"正在请求: {url}") # 上线前忘了删
response = requests.get(url)
print(f"状态码: {response.status_code}") # 又没删
return response.json()
# ✅ logging 版本,上线后调整日志级别即可
import logging
logger = logging.getLogger(__name__)
def fetch_data(url):
logger.debug(f"请求 URL: {url}")
response = requests.get(url)
logger.info(f"响应状态: {response.status_code}")
return response.json()
日志文件 + 控制台双输出
python
import logging
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
# 控制台 handler
console = logging.StreamHandler()
console.setLevel(logging.INFO)
# 文件 handler(自动轮转)
from logging.handlers import RotatingFileHandler
file_handler = RotatingFileHandler(
"app.log", maxBytes=10*1024*1024, backupCount=5, encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
# 统一格式
formatter = logging.Formatter(
"%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
console.setFormatter(formatter)
file_handler.setFormatter(formatter)
# 注册到 logger
logger.addHandler(console)
logger.addHandler(file_handler)
# 使用
logger.info("应用启动")
4️⃣ pdb --- 标准库的交互式调试器
print 和 logging 搞不定时,就要上断点调试了。pdb 是 Python 自带的,不需要装任何东西。
标记断点
python
def complex_calculation(a, b):
result = a * b
import pdb; pdb.set_trace() # ⚡ 断点!执行到这里停住
result += 100
return result
complex_calculation(5, 3)
# 执行到 pdb.set_trace() 时进入交互模式:
# > xxx.py(5)complex_calculation()
# -> result += 100
# (Pdb)
pdb 常用命令速查表
| 命令 | 缩写 | 作用 | 示例 |
|---|---|---|---|
list |
l |
显示当前行附近的代码 | l、l 15,25 |
next |
n |
执行下一行(不进入函数) | n |
step |
s |
进入函数内部 | s |
continue |
c |
继续执行到下一个断点 | c |
print |
p |
打印变量值 | p result |
pp |
pp |
美化打印 | pp data |
args |
a |
查看当前函数的参数 | a |
break |
b |
设置断点 | b 15、b foo.py:10 |
clear |
cl |
清除断点 | cl 1 |
where |
w |
查看调用栈 | w |
up |
u |
上移一层调用栈 | u |
down |
d |
下移一层调用栈 | d |
quit |
q |
退出调试器 | q |
! |
! |
执行 Python 语句 | !result = 999 |
实战调试会话
python
def parse_data(raw):
items = []
for i, line in enumerate(raw.split("\n")):
parts = line.split(",")
items.append({
"index": i,
"name": parts[0],
"value": int(parts[1]), # ❌ 这里可能会报错
})
return items
# 调试会话
data = "Alice,100\nBob,abc\nCharlie,200"
result = parse_data(data)
text
# 在 pdb 中的操作
> ... ValueError: invalid literal for int()...
# 重新运行,加断点
import pdb; pdb.set_trace() # 放在 parse_data 入口
(Pdb) b 6 # 在第 6 行(int())设断点
(Pdb) c # 继续执行
> ...\parse_data(6)...
(Pdb) p parts # 打印 parts 变量
['Bob', 'abc']
(Pdb) p parts[1] # 果然有问题
'abc'
(Pdb) !parts[1] = "200" # 临时修改变量
(Pdb) c # 继续,不再报错
5️⃣ ipdb --- 更好用的 pdb
ipdb 是 IPython 增强版,有语法高亮、Tab 补全、更直观的界面。
bash
pip install ipdb # 或者 uv pip install ipdb
python
import ipdb; ipdb.set_trace()
# 用法和 pdb 一样,但体验好太多了!
Python 3.7+ 的内置 breakpoint()
python
# 不需要 import 了!
def buggy_function(x):
result = x * 2
breakpoint() # 自动调用 pdb 或你设置的调试器
return result
设置环境变量可以切换调试器:
bash
# 使用 ipdb 代替 pdb
export PYTHONBREAKPOINT=ipdb.set_trace
# 禁用所有 breakpoint
export PYTHONBREAKPOINT=0
6️⃣ 调试综合实战
python
import logging
import ipdb
from typing import Optional
logger = logging.getLogger(__name__)
class UserService:
def get_user_scores(self, user_id: int) -> Optional[list[int]]:
"""获取用户分数,调试模式看全过程"""
logger.info(f"开始查询用户 {user_id} 的分数")
try:
raw_data = self._fetch_from_db(user_id)
logger.debug(f"原始数据: {raw_data}")
if raw_data is None:
logger.warning(f"用户 {user_id} 不存在")
return None
scores = self._parse_scores(raw_data)
logger.info(f"解析完成: {scores}")
return scores
except Exception as e:
logger.error(f"查询失败: {e}", exc_info=True)
# exc_info=True 会把完整 traceback 写入日志
raise
def _parse_scores(self, raw: str) -> list[int]:
"""如果这里出问题,用 breakpoint() 进入调试"""
breakpoint() # Python 3.7+ 断点
return [int(x.strip()) for x in raw.split(",")]
# 运行
service = UserService()
scores = service.get_user_scores(42)
⚡ 避坑 & 要点总结
| # | 要点 | 说明 |
|---|---|---|
| 1️⃣ | 按优先级选择调试方式 | print(快速验证)→ assert(防御检查)→ logging(日常监控)→ pdb/ipdb(疑难杂症) |
| 2️⃣ | 生产环境用 logging,别用 print | logging 可分级、可写文件、可远程收集 |
| 3️⃣ | assert 可能被禁用 | python -O 时所有 assert 跳过,别用于关键业务检查 |
| 4️⃣ | 用 breakpoint() 代替 import pdb; pdb.set_trace() |
更简洁,可切换调试器,Python 3.7+ 原生支持 |
| 5️⃣ | 日志记录异常用 exc_info=True |
把完整 traceback 写入日志,不丢上下文 |
| 6️⃣ | pdb 中可以改变量值 | 用 !result = 999 或直接赋值,用来继续执行验证 |
python
# 快速测试这段代码的调试行为
def test_me(x):
y = x * 2
breakpoint() # 运行到此处会停住
z = y + 10
return z
print(test_me(5))