Python 调试技巧:从 print 到 pdb 的进化之路
本文最后更新于 29 天前,如有失效请评论区留言。

刚学编程那会儿,我的调试方法就一招:print()。变量不对?print 一下。程序崩溃?在每个函数入口 print 一下。后来代码越来越复杂,print 满天飞,自己都看不懂哪条是哪条……直到有一天室友跟我说”你为啥不用 debugger?”,我才意识到自己一直在用石器时代的工具。

这篇笔记整理了我从 print 调试到 pdb 再到 logging 的进化过程,希望对你有点用。

print 调试:简单粗暴但有效

先别笑,print 调试其实没那么丢人。很多资深程序员在快速验证假设的时候也会用。问题不在于用 print,而在于怎么用得更好。

一个常见的坑:

你写了 print(x),输出了个 42。然后你在想:这个 42 是哪个地方输出的?是你刚才加的那个 print,还是之前调试忘删的那个?

所以第一条规则:给 print 加标签

# ❌ 别这样
print(x)
print(result)

# ✅ 至少加个标识
print(f"[DEBUG] x = {x}")
print(f"[DEBUG] result type = {type(result)}, value = {result}")

再进一步,可以用 inspect 模块自动打印调用位置:

import inspect

def debug_print(*args):
    frame = inspect.currentframe().f_back
    filename = frame.f_code.co_filename
    lineno = frame.f_lineno
    print(f"[DEBUG {filename}:{lineno}]", *args)

# 用法
x = 42
debug_print("x =", x)
# 输出: [DEBUG main.py:15] x = 42

说实话这个小工具我用了挺久,简单场景完全够用。但当项目变大、需要控制输出级别、或者想把调试信息写到文件里的时候,就得换工具了。

assert:程序里的”契约”

assert 是一个被严重低估的调试工具。它不只是”检查条件”,更像是一种文档化的假设——你在告诉未来的自己(和队友):”我觉得到这一步的时候,这个条件一定成立。”

def process_data(data):
    assert isinstance(data, list), f"expected list, got {type(data)}"
    assert len(data) > 0, "data should not be empty"
    
    result = sum(data) / len(data)
    assert 0 <= result <= 100, f"average out of range: {result}"
    return result

几个使用技巧:

  • 函数入口做 前置条件检查(输入类型、范围)
  • 函数出口做 后置条件检查(返回值符合预期)
  • 复杂逻辑中间做 不变量检查(中间状态符合预期)
  • 永远给 assert 加错误信息,否则失败的时候你只看到一行 AssertionError,啥也猜不出来

注意:Python 用 -O 参数运行时会跳过所有 assert!所以不要用 assert 做业务逻辑的校验,那是 if + raise ValueError 的活。assert 只用于开发阶段的调试断言。

pdb:交互式调试器

pdb 是 Python 自带的调试器,不用装任何东西。一开始我觉得命令行调试器太原始了,但用熟之后发现它在很多场景下比 IDE 调试器更方便——尤其是远程服务器上调试的时候。

基本用法

最简单的方式是在你想暂停的地方插入一行:

def calculate_something(a, b):
    result = a * b
    breakpoint()  # Python 3.7+ 推荐写法
    return result + 10

运行程序后会自动进入 pdb 交互界面。常用命令:

(Pdb) p result        # 打印变量
(Pdb) p a, b          # 同时打印多个
(Pdb) l               # 显示当前代码位置附近
(Pdb) n               # 执行下一行 (next)
(Pdb) s               # 进入函数内部 (step into)
(Pdb) c               # 继续运行 (continue)
(Pdb) w               # 打印调用栈 (where)
(Pdb) h               # 帮助

进阶技巧

pdb 其实可以直接在 Python 表达式上求值,不用加 print

(Pdb) type(result)
<class 'int'>
(Pdb) [x for x in data if x > 0]  # 直接跑列表推导
[1, 5, 9]
(Pdb) !some_var = 100  # 用 ! 前缀修改变量(避免和 pdb 命令冲突)

另一个实用功能是条件断点——你不想每次都停,只在特定条件下停:

# 在代码里
breakpoint(condition=lambda: len(data) > 100)

# 或者在 pdb 里设置
(Pdb) break 42, len(data) > 100

Python 3.7 之前用 import pdb; pdb.set_trace(),现在直接 breakpoint() 就行,简洁多了。

logging 模块:生产级的 print

当项目过了原型阶段,就该把 print 换成 logging 了。logging 的好处是可控——你可以设置级别、输出格式、输出目标,上线的时候把 debug 信息关掉而不删代码。

import logging

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

logger = logging.getLogger(__name__)

def process(data):
    logger.debug(f"收到数据: {len(data)} 条")
    result = transform(data)
    logger.info(f"处理完成, 结果: {result}")
    return result

日志级别从低到高:DEBUG < INFO < WARNING < ERROR < CRITICAL。开发时设 DEBUG 看一切,上线后设 WARNING 只看异常。

一个实际的项目配置通常会同时输出到控制台和文件:

import logging

logger = logging.getLogger('myapp')
logger.setLevel(logging.DEBUG)

# 控制台 handler — 只显示 INFO 以上
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter('[%(levelname)s] %(message)s'))

# 文件 handler — 记录所有
file_handler = logging.FileHandler('debug.log')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
    '%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s'
))

logger.addHandler(console)
logger.addHandler(file_handler)

其他实用工具

traceback:程序崩溃后的现场

程序报错的时候,traceback 信息是你最重要的线索。Python 的 traceback 默认已经很有用了,但你可以用 traceback 模块做更多事情:

import traceback

try:
    risky_operation()
except Exception:
    # 打印完整调用栈
    traceback.print_exc()
    
    # 或者保存到字符串
    error_msg = traceback.format_exc()
    logger.error(f"操作失败:n{error_msg}")

icecream:更好用的 print 替代品

icecreampip install icecream)是我最近发现的一个小工具,它的 ic() 函数会自动打印变量名和值:

from icecream import ic

x = 42
y = [1, 2, 3]
ic(x)
# 输出: ic| x: 42
ic(y)
# 输出: ic| y: [1, 2, 3]
ic(x + len(y))
# 输出: ic| x + len(y): 45

比手动写 print(f"x = {x}") 方便太多了。而且上线前可以用 ic.disable() 一键关闭所有输出。

远程调试:当本地复现不了的时候

有些 bug 只在服务器上出现。这时候几个选择:

  • rpdb:远程 pdb,通过 telnet 连接。装 pip install rpdb,代码里 import rpdb; rpdb.set_trace(),然后 telnet localhost 4444
  • 远程日志:把 logging 配置成发送到远程服务器(用 logging.handlers.SocketHandler
  • core dump + pdb:程序崩溃后拿到 dump 文件离线分析

调试的一般流程

折腾了这么多工具之后,我觉得调试还是有套路的:

  1. 重现:先稳定复现 bug,搞清楚触发条件
  2. 缩小范围:二分法注释代码,定位是哪一块出的问题
  3. 假设 + 验证:提出假设("我觉得这里 x 是 None"),用 assert/pdb 验证
  4. 理解根因:不要只修表面现象,搞清楚为什么 x 会变成 None
  5. 加回归测试:写个测试用例确保这个 bug 不会再出现

"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it." — Brian Kernighan

总结

不同的调试工具适合不同的场景,不存在万能方案:

  • print / ic:快速验证假设,脚本级别调试
  • assert:文档化假设,防御性编程
  • pdb:交互式调试,需要单步跟踪的时候
  • logging:项目级别,需要持久化和分级控制
  • traceback:事后分析,崩溃现场

我的建议是先把 pdb 用熟,它是免费的、随 Python 附带的,而且在很多场景下比 IDE 的图形调试器更灵活。等项目变大了再上 logging。至于 print?留着也没问题,别满屏都是就行。

版权声明:除特殊说明,博客文章均为原创,依据CC BY-SA 4.0许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇