tqdm 在批量下载、数据处理、模型训练等脚本里几乎是默认配置。单层进度条能解决“整体跑到哪了”,但遇到外层目录、内层文件或外层 epoch、内层 batch 的嵌套循环时,一条进度条很难同时表达整体进度和局部进度。本文围绕 tqdm 的双层进度条展开,先讲清楚 position、leave、bar_format、mininterval 等关键参数,再对比三种实现方案,并给出一份可运行的批处理示例,最后整理错行、日志冲突、性能下降和 IDE 兼容等常见问题。文中的代码逻辑在 Python 3.10 和 tqdm 4.66 环境下验证过。
一、双层进度条到底解决什么问题
安装 tqdm 只需要一行命令:
tqdm 的名字来自阿拉伯语 taqaddum,意思是进步、前进,并不是某个英文单词的缩写。最普通的用法是直接包住可迭代对象:
- from tqdm import tqdm
- import time
- for i in tqdm(range(100)):
- time.sleep(0.01)
复制代码
运行后会显示百分比、已完成数、总任务数和预估剩余时间。加上 desc 参数可以给进度条加前缀标签,例如:
- for i in tqdm(range(100), desc="处理数据"):
- time.sleep(0.01)
复制代码
这些基础用法已经能覆盖不少场景。真正麻烦的是嵌套任务:假设脚本先遍历 100 个文件夹,每个文件夹再处理 5000 行数据。只用外层进度条,看不到当前文件夹内部处理到哪一行;只用内层进度条,又不知道整体还剩几个文件夹;两条进度条如果都不做位置管理,还会互相覆盖,终端画面很快乱掉。
实际项目里对双层进度条有明确需求的场景包括:数据集遍历,外层目录、内层文件或批次;多阶段流水线,外层阶段、内层具体任务;批量上传下载,外层文件列表、内层单个文件分片;模型训练,外层 epoch、内层 batch,外层还要显示 loss;爬虫任务,外层分类页、内层详情页。它们的共同点是任务存在层级关系,单条进度条无法同时表达整体和局部两个维度。
我对“可用”的双层进度条有三个判断标准:第一,外层和内层各占固定位置,互不干扰;第二,内层任务结束后屏幕不堆残留行,进度条能自动清理;第三,日志可以穿插在进度条之间,但不会撕裂画面。后面的参数和方案都围绕这三条展开。
二、四个必须先弄懂的关键参数
1. position 与 leave:进度条的行号和去留
tqdm 默认在同一行反复刷新,依赖回车符把光标拉回行首再覆盖写入。创建第二条进度条时,如果不指定位置,两条进度条会争抢同一块画布。position 参数用于指定进度条占用的终端行位置,0 表示第一行,1 表示第二行,以此类推。leave 参数控制进度条结束后是否保留:leave=True 保留最终状态,leave=False 在任务完成后清掉该行。
可以把 position 理解成人为指定 tqdm 输出的物理行号,这也是排查错行问题的第一检查点。
2. 自动更新与手动更新的边界
平时写 tqdm(range(100)),每迭代一次自动更新一步,不需要手动干预。手动模式则只负责显示,进度前进由 update() 决定:
- with tqdm(total=100, desc="处理中") as pbar:
- while not done:
- step = do_something()
- pbar.update(step)
复制代码
手动模式在双层场景里很有用。例如下载文件时,内层任务不是简单按循环次数推进,而是按收到的字节数推进,自动模式无法直接表达,必须手动 update(已接收字节数)。内层任务结束后需要重新计数时,也可以配合 reset() 复用同一个进度条对象。
3. bar_format 控制进度条外观
bar_format 是控制进度条长相的核心参数,默认格式大致是 {l_bar}{bar}{r_bar} 三段式:左侧描述、中间进度条本体、右侧百分比和速度信息。双层进度条里,通常让外层精简,只显示百分比和总任务数;内层承担更完整的明细信息。
例如让外层只显示进度和已用时间:
- tqdm(total=100, position=0, bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}]")
复制代码
自定义 bar_format 时要保证花括号里的字段名合法,否则会直接抛 ValueError。
4. mininterval 与刷新性能开销
进度条本身也有开销。每次刷新都要向终端写入控制字符,如果循环体执行极快,刷新反而会成为瓶颈。双层进度条里内层循环往往转得很快,默认刷新策略可能导致肉眼可见的卡顿。
解决方向有两个:设置 mininterval,例如 mininterval=0.5 表示至少间隔 0.5 秒刷新一次;设置 miniters,表示最小更新步数间隔,达到一定步数后才真正重绘。tqdm 还内置 dynamic_miniters,会根据任务总耗时自动估算刷新间隔。大多数场景不用手动改,但如果加了进度条后程序明显变慢,优先怀疑刷新频率,而不是 tqdm 本身。
三、三种实现双层进度条的主流方案
方案一:嵌套循环直接套用
最直观的写法是把两个 tqdm 套在一起:
- from tqdm import tqdm
- import time
- for i in tqdm(range(10), desc="外层"):
- for j in tqdm(range(20), desc="内层"):
- time.sleep(0.01)
复制代码
这种写法的问题是,每次内层循环结束都会留下一行残留,跑完外层后终端会被大量进度条历史刷屏。给内层加上 leave=False 可以缓解:
- for i in tqdm(range(10), desc="外层"):
- for j in tqdm(range(20), desc="内层", leave=False):
- time.sleep(0.01)
复制代码
内层结束后会从屏幕上消失,画面干净一些。但内外层在物理位置上并没有真正固定,内层刷新时仍可能把外层信息顶上去,整体处于动态挤占状态,离稳定美观还有距离。
方案二:用 position 把两条进度条钉在不同行
这是生产环境里最常用的方案。通过 position 让内外层各占一行:
- from tqdm import tqdm
- import time
- outer = tqdm(range(10), desc="外层", position=0, leave=True)
- for i in outer:
- inner = tqdm(range(20), desc=f"内层-第{i}个", position=1, leave=False)
- for j in inner:
- time.sleep(0.01)
复制代码
执行时外层固定第一行,内层固定第二行。内层刷新只动第二行,外层纹丝不动;leave=False 保证内层完成后第二行被清空。外层先创建 tqdm 对象而不是直接写在 for 里,是因为外层要贯穿整个循环,内层要反复创建销毁,拆开管理更清晰。外层用 for i in outer 迭代,迭代结束外层自动关闭。如果需要三层,position 继续排到 2 即可,tqdm 对层级数没有硬性限制,真正限制你的是终端高度。
方案三:手动模式配合 reset 复用内层
position 方案每次内层循环都重新创建进度条对象,在极高频场景下会有对象创建开销。更紧凑的方式是提前建好内层对象,用 reset() 重置后再更新:
- outer = tqdm(total=10, position=0, desc="外层")
- inner = tqdm(total=20, position=1, desc="内层", leave=False)
- for i in range(10):
- inner.reset()
- for j in range(20):
- time.sleep(0.01)
- inner.update(1)
- outer.update(1)
复制代码
reset() 会把当前值清零、计时归零,但保留 desc 等配置。同一个对象可以重复使用,性能上比方案二更优,代码也更紧凑。手动模式还适合多条进度条并行独立驱动的场景,例如同时监控三个任务,position 分别设为 0、1、2,画面就像一个小型任务仪表盘。
四、完整实战:用双层进度条做批处理任务
下面用一个容易理解的例子收束前面的知识点:模拟批量下载 100 个文件,每个文件分为 50 个分片下载。外层显示 100 个文件的整体进度,内层显示当前文件内部 50 个分片的进度。每个文件开始下载时,还要在进度条区域之外打印一行文件名日志,并且不能把进度条画面撕碎。
完整代码:
- import random
- import time
- from tqdm import tqdm
- def download_one_file(file_id: int, total_chunks: int = 50) -> None:
- """模拟下载单个文件,按分片逐个完成"""
- inner = tqdm(
- total=total_chunks,
- position=1,
- desc=f"文件{file_id}",
- ncols=80,
- leave=False,
- bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt}",
- )
- for chunk in range(total_chunks):
- time.sleep(random.uniform(0.005, 0.02))
- inner.update(1)
- inner.close()
- def main() -> None:
- total_files = 100
- outer = tqdm(
- total=total_files,
- position=0,
- desc="批量下载",
- ncols=80,
- bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}]",
- )
- for file_id in range(total_files):
- tqdm.write(f"开始下载文件{file_id}")
- download_one_file(file_id)
- outer.update(1)
- time.sleep(0.01)
- outer.close()
- if __name__ == "__main__":
- main()
复制代码
运行时终端大致会出现三层信息:第一行是外层总进度,第二行是内层当前文件进度,第三行是 tqdm.write 打印的日志。每下载完一个文件,第二行会被清除,下一行日志打印出来,接着新的内层进度条再次出现在第二行,画面稳定且干净。
这里最关键的是 tqdm.write。它和 print 的区别在于:print 会直接往终端写一行字符,容易把进度条画面顶乱;tqdm.write 会先清理进度条输出区域,写完日志再重新绘制进度条。只要涉及在进度条旁边输出日志,优先使用 tqdm.write。
两个容易被忽略的细节:第一,ncols=80 是故意设置的。不指定时 tqdm 会自适应终端宽度,在 IDE 内置终端、SSH 窗口宽度变化等场景下,进度条长度会频繁调整,内外层长度不一致时画面会轻微抖动,固定宽度能从根源上消除这类问题。第二,外层不用 for i in tqdm(range(100)) 这种自动写法,而是手动创建 tqdm 对象再 update,因为外层循环体里还要调用内层函数,日志输出时机也需要精确控制,手动模式颗粒度更细。
如果使用 Jupyter Notebook,应改用:
- from tqdm.notebook import tqdm
复制代码
notebook 版本渲染的是前端控件,嵌套使用时视觉上会分成上下两块区域,互不打扰。但要注意:任务量极大、单元格输出频繁时会有明显的前端渲染开销;日志仍推荐用 tqdm.write,或者写入文件后再从外部 tail 查看。
五、常见问题与排查技巧
1. 进度条乱跳、错行
现象是外层和内层文字互相覆盖,或者进度条跑到屏幕上方。多数情况是 position 没设置,或者内外层用了相同的 position。tqdm 刷新时按照 position 寻找指定行,两层都占 0 号位就会打架。
解法:外层 position=0、内层 position=1;创建对象时固定好 position,不要在循环中途修改;内层频繁创建销毁时记得 leave=False,否则第二行会越积越多。另一个隐蔽原因是脚本启动前终端里残留了大量历史输出,进度条会把这些输出往上顶,看起来像跑偏。建议启动前清屏,或者从一开始就用 tqdm.write 接管日志。
2. 日志和进度条互相打架
现象是 print 一行日志,进度条画面就被撕裂一次。原因是 print 和 tqdm 在争夺终端同一块画布,tqdm 依赖回车刷新当前行,print 依赖换行,混用会让输出序列失序。
解法:统一用 tqdm.write。如果第三方库内部强行调用 print,可以临时重定向输出,或者在库调用前后手动重建进度条。实际使用中 tqdm.write 能覆盖绝大多数需求,重定向方案往往代码丑陋且不稳定。
3. 加了进度条之后程序明显变慢
现象是业务逻辑没变,只是加了进度条,运行时间拉长数倍。原因通常是刷新频率过高。默认情况下 tqdm 每次 update 都会尝试刷新,如果循环体执行时间比刷新动作还短,进度条就成了拖后腿的那个。
解法:设置 mininterval=0.1 或 0.5 降低刷新频率;设置 miniters 控制最小更新步数;把 disable 参数接到环境变量上,例如调试场景直接关闭进度条。经验上,几秒内完成的任务不套进度条,直接用 tqdm.write 打印结果;几十秒以上的任务才值得上进度条。
4. 进度条闪屏和 IDE 环境适配
现象是进度条肉眼可见地闪烁,尤其在 Windows 老版 cmd 或部分 SSH 终端里。原因是终端对回车符支持不完整,加上 tqdm 默认动态宽度,每次刷新都可能触发整行重排。
解法:设置 ncols 固定宽度,设置 mininterval=0.2 降低刷新频率。如果仍闪,考虑更换终端,实测 Windows Terminal 比老 cmd 稳定。PyCharm 的 Run 窗口也不完全是标准终端,进度条经常一行一行往下堆,此时可设置环境变量 PYCHARM_HOSTED=1,或者依赖 tqdm 的自适应模式,它会检测到不支持回车刷新的环境并退化为按行打印。GitHub Actions 等 CI 环境同理,日志系统会把回车吞掉,密集进度条会产生海量日志,这种情况下建议直接 disable=True。
5. 版本差异和异常处理
tqdm 更新速度不慢,各版本之间的默认行为可能有小差异,例如早期版本对 notebook 支持不稳定,color 参数在某些终端不生效。遇到诡异表现时,先升级到最新版,再检查终端类型。很多奇怪问题最后都能归到版本和终端两个变量上。
六、几个长期使用后形成的习惯
第一,进度条的 desc 尽量带动态信息。例如内层写成 f"文件{file_id}",比写死的“内层”有用得多,出问题时能直接从进度条判断卡在哪个任务上。第二,进度条用完随手 close。虽然对象回收时也能自动关闭,但显式 close 可以确保终端光标状态被正确恢复,避免脚本结束后终端残留异常光标行为。第三,把 tqdm 封装成统一工具函数,不要到处散落裸调用。例如封装一个 get_progress(total, desc, position),全局换样式时只改一处,维护成本会低很多。
如果复现时表现和文中不一致,优先检查两个变量:tqdm 版本和终端类型。它们对进度条显示的影响,通常比代码本身更大。 |