再用 print 调试 Python,你就 OUT 啦?
最近加班改一个小工具,我回头看自己代码,满屏全是:
print("step1", data)
print("after clean", clean_data)
print("here!!", len(result))
那一刻真的有点羞耻感:这不就是我刚学 Python 时的调试方式嘛,结果过了好几年,我还在干这事儿……
但话说回来,print 也不是不能用,只是很多场景下,它真的有点“拖后腿”了。
print 调试哪里开始不香的?你仔细想想,print 最大的问题其实有几个:
代码一不小心到处是调试语句,线上之前忘删,日志里全是奇怪的“到这儿了1”“到这儿了2”。
出问题的时候,你要在逻辑里到处插 print,改完再重新跑一遍;结果又发现某个位置少打了,只能继续加、再跑一遍,循环折磨。
多线程、异步、定时任务这类场景,print 的输出还会乱序,你根本看不出先后关系。
而且 print 只能傻傻往标准输出打,不分级别、不分模块,想过滤都没法精细控制。
简单说:print 更像一次性筷子,小脚本图一乐没问题,一上生产或复杂逻辑,就显得有点土了。
logging 取代大部分 print同样是“打点看值”,logging 比 print 高级很多,而且改成本几乎为零。
最简单的一套配置是这样:
import logging
logging.basicConfig(
level=logging.DEBUG, # 最低输出级别
format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger(__name__)
之后就可以把以前的 print 换成:
logger.debug("raw data: %s", data)
logger.info("user %s logged in", user_id)
logger.warning("retry %s times", retry_count)
logger.error("bad value: %s", value)
几个好处你很快就能体会到:
开发阶段把 level 设成 DEBUG,信息给足; 线上只要改个配置成 INFO 或 WARNING,一大堆细节日志自动被“静音”,不用你去删除那堆调试语句。
想把日志打到文件里也很简单:
logging.basicConfig(
level=logging.DEBUG,
filename="app.log",
filemode="a",
format="%(asctime)s [%(levelname)s] %(message)s"
)
同样是“看一眼变量”,logger.debug 比 print 优雅太多,而且不会变成以后维护代码的垃圾。
有些 bug 靠肉眼看日志,永远都看不出问题在哪,尤其是那种“跑到某一步突然就不对了”的情况。
这时候,用 Python 自带的调试器 pdb 会舒服很多。
最粗暴的用法就是在怀疑的地方插一句:
import pdb
defcalc_avg(nums):
total = sum(nums)
count = len(nums)
pdb.set_trace() # 下断点
return total / count
程序跑到 set_trace() 就会停下来,终端里变成一个交互式调试环境,你可以:
p nums # 打印变量
p total
p count
n # 执行下一行
s # 进入函数内部
l # 查看当前上下文代码
c # 继续运行到下一个断点
q # 退出调试
比如你线上偶尔出现除零异常,传进来的 nums 有时是空列表,用 print 你可能要打好几行来排查哪里传错了; 换成 pdb,程序直接停在出事之前,你当场看参数、看调用栈,一眼就知道是谁传了一个空值过来。
如果不想改代码插 set_trace(),也可以直接用:
python -m pdb main.py
按 b 行号 下断点,run 起来之后,照样可以一步一步走。
这里说的“算法”,不是让你上来就写什么 A*、DP、贪心那种,而是把调试这件事当成一个“搜索问题”。
比如一段数据处理流程,大致是这样:
defpipeline(data):
a = clean(data)
b = normalize(a)
c = extract_feature(b)
d = model_predict(c)
return post_process(d)
结果最后返回的结果总是不对。 很多人第一反应:在每一步后面 print 一下,print(a) print(b) ... 全打出来,眼都要看花。
更聪明的做法是用“二分法”:
先在中间打一个断点,看一眼 c 正不正常: 如果 c 就已经不对了,说明问题在前半段; 如果 c 是对的,那就去盯后半段。
代码配合 pdb 或 logging 来写,大概像这样:
defpipeline(data):
a = clean(data)
b = normalize(a)
import logging
logging.debug("after normalize: %s", b)
c = extract_feature(b)
d = model_predict(c)
return post_process(d)
或者直接:
import pdb; pdb.set_trace()
停在你想看的那一步。 每次只关心一小段逻辑,逐步缩小范围,调试效率会明显上去。
假设你有个函数是给一堆成绩算平均分,过滤掉负数和 None:
defavg_score(scores):
valid = [s for s in scores if s isnotNoneand s >= 0]
total = sum(valid)
return total / len(scores) # 这里其实写错了
有时候你发现结果特别小,但肉眼一看数据也挺正常。 很多人第一步就是加 print:
defavg_score(scores):
valid = [s for s in scores if s isnotNoneand s >= 0]
print("valid:", valid)
total = sum(valid)
print("total:", total)
print("len:", len(scores))
return total / len(scores)
输出一看,才发现自己居然拿的是 len(scores) 而不是 len(valid)。 这个 bug 还算好排。
但如果这段逻辑是嵌在一大堆数据预处理里,中间经过很多函数调用,你就得处处加 print,删也难删干净。
用 pdb 的话可以这样:
import pdb
defavg_score(scores):
valid = [s for s in scores if s isnotNoneand s >= 0]
total = sum(valid)
pdb.set_trace()
return total / len(scores)
程序跑到这里停住,你一行一行地敲:
p scores
p valid
p len(scores)
p len(valid)
差别立刻就出来了,而且你不用到处插调试语句,只在关键点停一次就够。
print 不是原罪,小脚本、临时验证、写算法题的时候,用 print 看一眼结果,很正常。
但一旦你写的是要长期维护、要上线跑的 Python 项目,就真的别再满屏 print("here") 了。 养成几个习惯:日志用 logging,排查疑难杂症用 pdb 或 IDE 断点,调试时脑子里有点“缩小范围”的算法思路。
调试这事儿,其实和写代码本身一样,是可以练级的。 下次你再看到同事到处乱打 print,就可以悄悄把这篇文章丢给 TA 了😄
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB