
前言长循环没有反馈是脚本体验差的头号原因跑了几分钟光标一动不动你分不清它在算、在等 I/O还是早就卡死了。tqdm这个第三方库就是来解决这件事的——把它套在可迭代对象外面终端里立刻出现一条实时刷新的进度条。先纠正两个常见误解。第一以为tqdm是标准库import tqdm就能用——它是第三方库需要先pip install tqdm。第二以为进度条是「一行一行往下打印」的——恰恰相反它的核心动作是往输出流写一个回车符再重绘同一行所以它不是「打印一行」而是「原地刷新」。理解这一点才能解释为什么把输出重定向到文件时文件里会塞满大量被回车覆盖的中间状态。本文讲清tqdm的几种基本用法包裹可迭代对象、手动update、total与desc、嵌套进度条并把它「往 stderr 写\r」的工作原理说明白。示例按tqdm4.x 的接口书写具体参数以该库官方文档为准。运行环境需要 Python 3Python 2.7 已于 2020-01-01 停止维护本文不适用。一、安装与最小用法pip install tqdm用tqdm包住任何可迭代对象即可# 适用于 Python 3.7需先 pip install tqdmfrom tqdm import tqdmimport timefor i in tqdm(range(100)):time.sleep(0.01)如果可迭代对象有__len__tqdm能自己算出总数于是进度条、百分比、剩余时间一应俱全。要是对象没有长度比如生成器、文件句柄就得自己告诉它总数否则只会显示「已处理 N 个」没有百分比。# 适用于 Python 3.7需先 pip install tqdmfrom tqdm import tqdmdef gen(n):for i in range(n):yield i * i# 生成器没有 len()用 total 显式给出总数for value in tqdm(gen(50), total50, desc计算平方):passtotal是「预期迭代次数」desc是进度条前面的文字说明。两者都是可选参数给与不给只影响显示不影响迭代本身。二、手动控制update 与 close当循环结构不是「一个个消费可迭代对象」时比如按批次处理、按字节数推进就用手动模式不传iterable自己调update()。# 适用于 Python 3.7需先 pip install tqdmfrom tqdm import tqdmimport timepbar tqdm(total1000, desc下载, unitKB)try:for _ in range(10):time.sleep(0.05)pbar.update(100) # 每次推进 100 个单位finally:pbar.close() # 循环结束或中途异常都要收尾几个要点total与update的单位要一致。上面total1000配每次update(100)十次刚好走满。unit只是显示用的单位名默认是it不参与计算。没有用with或for包裹时必须自己close()否则进度条可能停在半路、或者不显示最终 100% 的状态。想要「异常时也不留半条进度条」用with语句tqdm对象支持上下文管理器协议# 适用于 Python 3.7需先 pip install tqdmfrom tqdm import tqdmimport timewith tqdm(total100, desc处理) as pbar:for _ in range(10):time.sleep(0.02)pbar.update(10)也可以给tqdm传initial已完成的量用于「接上一次中断的进度继续」。循环过程中还能动态改描述# 适用于 Python 3.7需先 pip install tqdmfrom tqdm import tqdmwith tqdm(total3) as pbar:for name in [a.py, b.py, c.py]:pbar.set_description(f正在处理 {name})pbar.update(1)set_description()改的是前缀文字set_postfix()可以在右侧挂一组自定义键值例如当前耗时、命中率两者都只影响显示。三、嵌套进度条外层循环处理大阶段、内层处理小步骤时可以用嵌套进度条给每层一个position让它们各占一行。# 适用于 Python 3.7需先 pip install tqdmfrom tqdm import tqdmimport timefor epoch in tqdm(range(3), descepoch, position0):for step in tqdm(range(20), descstep, position1, leaveFalse):time.sleep(0.005)参数作用嵌套时的建议total预期总数每层都写清楚否则没有百分比desc前缀文字每层给不同文字便于区分position占第几行从 0 开始外层 0、内层 1逐层递增leave结束后是否保留进度条内层设False避免刷屏file输出到哪个流默认sys.stderr多进程时需分别指定官方文档在讲嵌套时提到应优先尝试让它自动判断位置只有在确实需要手动控制例如多进程场景时才显式指定positionn其中n0是最外层。leaveFalse表示这一层完成后把进度条从屏幕上擦掉内层用它能让输出干净很多。tqdm还提供了trange它是tqdm(range(...))的简写# 适用于 Python 3.7需先 pip install tqdmfrom tqdm import trangeimport timefor i in trange(10, desc简写):time.sleep(0.01)四、工作原理往 stderr 写回车重绘进度条能「原地刷新」靠的是一个控制字符——回车符\r也就是\r。终端收到\r会把光标移回本行行首注意是回到行首不是换行随后写出的内容就会覆盖原来那一行。tqdm的每一次刷新本质上就是回到行首 → 重新写一遍当前状态的文本。这也解释了它的几个默认行为默认输出到sys.stderr。文档写明file参数「指定进度信息的输出位置默认是sys.stderr」并且它使用file.write(...)与file.flush()。放在 stderr 上是为了不污染程序正常的 stdout 输出方便把真实结果重定向到文件时进度条仍显示在屏幕上。只有输出是终端时才好看。终端会解释\r所以你看到的是「一条不断变化的条」。一旦把 stderr 重定向到文件文件里没有光标每一次刷新都只是往文件末尾追加一段带\r的文本于是文件里会留下大量「残留」——几十上百份中间状态首尾相接。刷新有节流。为了不让高频循环把 CPU 耗在重绘上tqdm默认有最小刷新间隔mininterval默认为 0.1 秒与最小迭代数控制。所以哪怕循环一百万次屏幕上刷新的次数也远少于一百万次。# 适用于 Python 3.7需先 pip install tqdmfrom tqdm import tqdmimport sysimport time# 把进度条写进一个文件观察 \r 重绘在「非终端」上的后果with open(progress.log, w, encodingutf-8) as f:for i in tqdm(range(5), filef, mininterval0.0, miniters1):time.sleep(0.05)用一个纯粹的字符串来模拟这种「重绘」是什么感觉# 适用于 Python 3.8import sysimport time# \r 只把光标移回行首不换行新内容覆盖旧内容for percent in (0, 25, 50, 75, 100):sys.stdout.write(f\r进度{percent:3d}%)sys.stdout.flush() # 不 flush 可能一直不显示sys.stdout.write(\n) # 最后自己补一个换行让提示符回到下一行这段代码只用标准库就能看出进度条的骨架\r 覆盖 flush。正因为进度条会覆盖当前行日志和进度条混在一起时会互相踩。tqdm提供了pbar.write()/tqdm.write()它会先「让开」进度条再输出一行普通文字# 适用于 Python 3.7需先 pip install tqdmfrom tqdm import tqdmimport timewith tqdm(total3) as pbar:for name in [one, two, three]:time.sleep(0.02)pbar.write(f已处理 {name}) # 不破坏进度条的正常输出pbar.update(1)常见坑点坑点 1把tqdm当成标准库。❌ 直接from tqdm import tqdm却从没装过 —— 报ModuleNotFoundError。 ✅ 先pip install tqdm。装不了的环境里就用标准库自己写sys.stderr.write(\r...)加flush()。坑点 2把进度条输出重定向进日志文件。❌python job.py 2 log.txt—— stderr 里全是带\r的中间状态文件被大量残留撑大且难以阅读。 ✅ 明确让进度条在终端显示、日志走别的流或判断输出不是终端时用disableTrue关掉进度条。坑点 3手动模式下忘了total或忘了close()。❌pbar tqdm()后只update()循环完直接走人 —— 没有总数就没有百分比不close()可能看不到收尾状态。 ✅ 给出total并用with tqdm(total...) as pbar:或try/finally保证收尾。坑点 4拿iterable的同时在循环里break。❌ 提前break后进度条停在半路看起来像「失败了」。 ✅ 这是符合预期的——你没走完想让中断也干净用with包裹退出时会收尾。坑点 5以为update()的默认参数是 1 所以可以乱调。❌ 每次pbar.update(100)但total100—— 进度直接冲到 10000%显示乱了。 ✅update的步进单位与total的单位保持一致不确定就都用「个数」。坑点 6嵌套时不设position或不关内层leave。❌ 两层都用默认值 —— 两个进度条抢同一行画面错乱内层结束后留下一屏历史条。 ✅ 外层position0内层position1, leaveFalse官方建议先试自动定位不行再手动指定。坑点 7照着 Python 2 的老教程抄print语句。❌ 代码里写print 进度 str(i)—— Python 2.7 已于 2020-01-01 停止维护Python 3 里这是语法错误。 ✅ 用函数形式print(f进度{i})字符编码也统一走 Unicode 语义别再用u...当必需项。坑点 8在高频循环里把mininterval、miniters全设成 0。❌ 每次迭代都强制重绘 —— 刷新本身变成了可观的开销进度条反而拖慢程序。 ✅ 保留默认节流或按需调大mininterval要压吞吐量时宁可少刷新。总结你想做的事写法注意包裹 for 循环for x in tqdm(seq):有len才有百分比无长度的生成器tqdm(gen(), totalN)显式给总数手动推进pbar.update(n)记得close()改前缀文字pbar.set_description(text)只影响显示嵌套position0/1、内层leaveFalse先试自动定位打印日志不打架pbar.write(msg)会先让开进度条输出到别处file参数默认是sys.stderr关掉进度条disableTrue非终端环境推荐tqdm的全部魔法其实就一句话往流里写\r回到行首再重绘同一行。默认写sys.stderr、默认带节流都是为了「既看得见、又不干扰结果」。一旦离开终端重定向到文件、被 CI 收集日志\r就失去意义、只剩残留这时候正确的做法是disable或改走日志而不是把进度条硬塞进文件。