
用过tqdm的朋友应该都有同感这个库一旦用上就再也回不去了。不管是训练模型、批量下载文件还是处理上百万条数据一条进度条比什么日志都好使。但单层进度条总有不够用的时候——当你面对的是嵌套循环比如外层遍历文件夹、内层遍历每个文件里的记录单单一条进度条根本没法告诉你“现在到底进行到哪一步了”。这时候就需要双层进度条外面一层显示整体任务进度里面一层显示当前子任务的细粒度进度。这篇文章我会从tqdm底层参数讲起对比三种双层进度条的实现方案最后给出一份可以直接抄作业的完整代码并把实际使用中踩过的坑一并整理出来。适合已经会用tqdm跑单层进度条、想进一步把终端输出面板收拾得干净利落的朋友哪怕你只是听说过tqdm跟着读也不会有压力。1. 双层进度条到底解决了什么问题1.1 先回顾tqdm的家常用法tqdm的名字来自阿拉伯语“taqaddum”本意是“进步、前进”并不是某个英文单词的缩写。安装只需要一行命令pip install tqdm最普通的用法是直接包住一个可迭代对象让它自动更新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)大部分读者应该早就用过这些基础玩法了这里不啰嗦。真正要展开的是当任务变成嵌套循环时进度条该怎么管理。1.2 单层进度的局限性和双层需求举个具体的例子。假设我要写一个批量处理脚本先遍历100个文件夹每个文件夹里再处理5000行数据。如果只用单层进度条问题立刻暴露——只显示外层时看不到当前文件夹内部处理到了第几行只显示内层时又不知道整体还剩几个文件夹两条进度条不加管理地叠在一起画面就会互相覆盖非常混乱。类似的情况几乎每个写脚本的人都会遇到。我整理了一下实际项目里对双层进度条有硬需求的场景大致有这几类数据集遍历外层遍历目录内层遍历文件或批次。多阶段流水线外层是阶段内层是阶段内的具体任务。批量上传下载外层是文件列表内层是单个文件的传输分片。模型训练外层是epoch轮数内层是batch批次外层还要实时显示loss等信息。爬虫任务外层遍历分类页内层遍历详情页。这些场景的共同点是任务存在明确的层级关系单靠一条进度条无法同时表达“整体进度”和“局部进度”两个维度。双层进度条本质上解决的就是这种信息维度不足的问题。1.3 双层进度条的评判标准既然要做“美观”的双层进度条就得先明确什么叫美观。以我的标准来看有三条第一外层和内层各占固定位置互不干扰第二内层任务结束后屏幕不乱堆残留行进度条自动清理第三日志输出可以穿插在进度条之间但不会撕裂画面。后面所有的方案和参数讲解都是围绕这三个标准展开的。2. 动手之前需要吃透的四个关键参数进入正式实现前必须先弄明白几个和“多层展示”直接相关的参数。不夸张地说双层进度条出问题九成都是栽在这几个参数上。2.1 position与leave进度条的行号和去留tqdm默认的行为是在控制台同一行反复刷新刷新机制依赖回车符\r把光标拉回行首再覆盖写一行。当你创建第二条进度条时如果不做任何位置控制两条进度条会争抢同一块画布终端画面会乱掉。position参数的作用就是直接指定进度条在终端中占用的行位置0表示第一行1表示第二行以此类推。leave参数则控制进度条结束后是否保留leaveTrue保留最终状态leaveFalse在任务完成后把该行清掉。这里最关键的理解是tqdm本质上在向终端的光标位置输出position相当于人为指定了它的物理行号。这个机制是后面所有双层甚至三层进度条方案的地基也是排查错行问题时的第一检查点。2.2 自动更新与手动更新的适用边界平时用tqdm(range(100))这种方式进度条每迭代一次自动更新一步完全不需要手动干预。手动模式则不同它只负责显示进度是否前进完全由你自己调用update()决定with tqdm(total100, desc处理中) as pbar: while not done: step do_something() pbar.update(step)手动模式最大的价值在两层场景里体现得最明显。比如下载文件时内层任务不是简单按循环次数推进而是按收到的字节数推进自动模式就无能为力了必须手动update(已接收字节数)。再比如内层任务结束后需要重置进度条重新计数的场景手动模式配合reset()方法可以实现同一行反复使用。2.3 bar_format控制进度条长相bar_format是控制进度条外观的核心参数默认格式大致是{l_bar}{bar}{r_bar}三段式左侧描述文字、中间进度条本体、右侧百分比和速度信息。在双层进度条里我通常会让外层保持精简只显示百分比和总任务数让内层承担更完整的明细信息。自定义格式的写法很简单比如把外层设计成只显示进度和已用时间tqdm(total100, position0, bar_format{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}])需要注意的是自定义bar_format时必须保证花括号里的字段名合法否则会直接抛ValueError。我踩过一次把total_fmt拼成total_fm的坑报错信息倒还算友好但凭空浪费了几分钟。2.4 mininterval与刷新性能开销很多读者可能从没想过一个问题进度条本身也是有开销的。每次刷新都要向终端写入一长串控制字符如果循环体执行得极快比如一次循环只需要微秒级进度条的刷新反而会成为性能瓶颈。这个现象在双层进度条里尤其明显因为内层循环往往转得非常快默认刷新策略会导致肉眼可见的卡顿。解决办法有两个方向一是设置mininterval比如mininterval0.5表示至少间隔0.5秒才刷新一次视觉上完全不受影响但性能开销大幅下降二是设置miniters表示最小更新步数间隔只有在累计更新达到一定步数后才真正触发一次重绘。tqdm还内置了dynamic_miniters机制它会根据任务总耗时自动估算合理的刷新间隔。大多数场景下不用手动干预但如果你加了进度条后明显感觉程序变慢优先怀疑刷新频率而不是tqdm本身。3. 三种实现双层进度条的主流方案参数都弄明白之后实现方案其实就水到渠成了。我按代码写法和使用场景的不同把主流做法分成三种。3.1 方案一嵌套循环直接套用最直观的写法就是在代码层面把两个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)这段代码在终端里的表现是内层进度条反复刷新外层进度条立在原地等待。问题是每次内层循环结束内层进度条都会在屏幕上留下一行残留跑完10个外层任务之后终端会被二三十行进度条历史刷屏。解决办法是给内层加上leaveFalsefor i in tqdm(range(10), desc外层): for j in tqdm(range(20), desc内层, leaveFalse): time.sleep(0.01)这样内层进度条每次结束后会从屏幕上消失画面干净不少。但这个方案有个硬伤内外层在物理位置上并没有真正固定内层刷新时会把外层信息顶上去整体还是处于“动态挤占”状态离“美观”还有距离。3.2 方案二用position参数把两条进度条钉在不同行这是我个人最推荐、也是生产环境里用得最多的方案。通过position参数让内外层各占一行互不打扰from tqdm import tqdm import time outer tqdm(range(10), desc外层, position0, leaveTrue) for i in outer: inner tqdm(range(20), descf内层-第{i}个, position1, leaveFalse) for j in inner: time.sleep(0.01)执行效果是外层固定第一行内层固定第二行。内层刷新只会动第二行外层纹丝不动leaveFalse保证内层完成以后第二行被清空全程画面始终只有两条进度条在交替出现。有一点值得专门强调这个写法里外层是先创建了tqdm对象而不是直接写在for关键字里。原因是这两条进度条都有独立的生命周期外层要贯穿整个循环内层要反复创建销毁把它们拆开管理视野会清晰得多。外层用for i in outer迭代迭代结束外层自动关闭。如果你需要三层逻辑完全一样position继续往后排到2就行了tqdm对层级数没有硬性限制真正限制你的是终端高度。3.3 方案三手动模式配合reset实现内层复用position方案虽然好用但每次内层循环都重新创建一条进度条对象在极高频场景下会有一定的对象创建开销。更优雅的方式是提前把内层进度条对象建好用reset()方法重置后再更新outer tqdm(total10, position0, desc外层) inner tqdm(total20, position1, desc内层, leaveFalse) 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每条进度条用自己的循环单独更新画面就像一个小型任务仪表盘。这在自动化运维脚本里非常实用。4. 完整实战用双层进度条做一个批处理任务理论讲完给一个能直接复制运行的完整场景。我挑选一个大家都能快速理解的例子模拟批量下载100个文件每个文件分为50个分片下载。4.1 需求拆解与设计思路先拆解需求外层需求是展示100个文件的整体下载进度内层需求是展示当前文件内部50个分片的下载进度。同时每个文件开始下载时需要在进度条区域之外打印一行文件名日志而且日志不能把进度条画面撕碎。这其实是一个相当典型的双层进度条实战问题里面几乎浓缩了前面讲过的所有知识点固定position、leave控制、tqdm.write输出日志、手动update、ncols固定宽度。4.2 完整代码实现import random import time from tqdm import tqdm def download_one_file(file_id: int, total_chunks: int 50) - None: 模拟下载单个文件按分片逐个完成 inner tqdm( totaltotal_chunks, position1, descf文件{file_id}, ncols80, leaveFalse, 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( totaltotal_files, position0, desc批量下载, ncols80, 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()4.3 运行效果与关键细节解释这段代码运行时的终端画面大致是这样三层批量下载: 46%|██████▌ | 46/100 [00:3500:41] 文件47: 38%|█████ | 19/50 开始下载文件47第一行是外层总进度第二行是内层当前文件进度第三行是tqdm.write打印的日志。每下载完一个文件第二行会被清除下一行日志打印出来接着新的内层进度条再次出现在第二行。整个画面稳定、干净不会出现进度条互相顶替的情况。这里最核心的按钮其实是tqdm.write。它跟print最大的区别在于print会直接往终端写一行字符把当前的进度条画面顶乱tqdm.write会先把进度条的输出区域暂时清理掉写完日志再重新绘制进度条。所以只要涉及进度条旁边输出日志一律用tqdm.write这条规则基本可以记死。4.4 两个容易被忽略的细节第一ncols80是我故意设置的。如果不指定tqdm会自适应终端宽度。在本地命令行没问题但在IDE内置终端、SSH窗口宽度变化等场景下进度条的长度会跟着终端宽度频繁调整内层和外层长度不一致时画面会轻微抖动。固定宽度从根源上消除了这类问题。第二外层为什么不用for i in tqdm(range(100))这种自动写法而是手动创建tqdm对象再update因为外层循环体里除了更新自身进度还要调用内层函数而且日志输出时机也需要精确控制。手动模式的颗粒度更细虽然代码多了两行但可读性和可控性都更好。4.5 Jupyter Notebook环境下的特殊处理如果你用的是Jupyter Notebook情况会有点不一样。Notebook的单元格不是标准终端直接from tqdm import tqdm往往表现不正常应该改用from tqdm.notebook import tqdm这个版本渲染的是前端控件嵌套使用时视觉上会分成上下两块区域互不打扰体验比命令行版本更直观。但要提醒两点第一notebook版本在任务量极大、单元格输出频繁时会有明显的前端渲染开销第二notebook里的日志输出推荐还是用tqdm.write或者干脆把日志写进一个文件再从外部tail查看比堆在单元格里清爽得多。5. 常见问题与排查技巧实录这部分内容是我在真实项目里踩过坑之后总结的速查表按照“现象-原因-解法”的顺序写方便你遇到问题时直接对号入座。5.1 进度条乱跳、错行的根本原因现象外层和内层的文字互相覆盖或者进度条跑到屏幕上方去了。原因绝大多数情况是position参数没设置或者内外层用了相同的position。tqdm刷新时按照position值寻找指定行两层都占0号位自然互相打架。解法也很直接严格执行外层position0、内层position1的约定创建进度条对象时就把position固定好不要在循环中途修改内层频繁创建销毁时记得leaveFalse否则第二行会越积越多。还有一个隐蔽原因常常被忽略脚本启动进度条之前终端里可能残留了大量历史输出。进度条会把这些输出往上顶看起来就像进度条“跑偏了”。建议启动前清屏或者干脆从第一行日志开始就用tqdm.write接管。5.2 日志和进度条互相打架现象print一行日志进度条画面就被撕裂一次越打越乱。原因print和tqdm都在争夺终端同一块画布。tqdm基于\r回车刷新当前行print基于\n换行二者混用会让输出序列失序。解法统一用tqdm.write。如果某些第三方库内部强行调用print你可以临时重定向它们的输出或者在库调用前后手动重建进度条。实测下来tqdm.write能覆盖几乎所有需求重定向方案代码又丑又不稳定不推荐。5.3 加了进度条之后程序明显变慢现象业务逻辑没变只是加了进度条运行时间拉长了数倍。原因刷新频率过高。默认情况下tqdm每次update都会尝试刷新如果循环体执行时间比刷新动作还短进度条就成了拖后腿的那个。解法设置mininterval0.1或0.5降低刷新频率设置miniters控制最小更新步数再加一层保险把disable参数接到环境变量上比如debug场景下直接关闭进度条。我自己的习惯是任务总耗时在几秒以内不套进度条直接用tqdm.write打印结果几十秒以上的任务才值得上进度条不是所有循环都适合加进度条。5.4 进度条闪屏和IDE环境适配现象进度条肉眼可见地闪烁尤其在Windows老版cmd或某些SSH终端里。原因终端对\r回车的支持不完整加上tqdm默认动态宽度每次刷新都可能触发整行重排。解法设置ncols固定宽度设置mininterval0.2降低刷新频率。如果还闪考虑换终端实测Windows Terminal比老cmd稳定太多。在PyCharm的Run窗口里也有坑它不完全是标准终端进度条经常一行一行往下堆。此时设置环境变量PYCHARM_HOSTED1或者依赖tqdm的自适应模式它会检测到不支持\r的环境并自动退化为按行打印。GitHub Actions等CI环境同理日志系统会把\r吞掉密集进度条会产生海量日志这种情况下建议直接disableTrue。5.5 版本差异和异常处理最后提一句版本问题。tqdm更新速度不算慢各版本之间的默认行为有小差异比如早期版本对notebook支持不稳定color参数在某些终端也不生效。遇到诡异表现时先升级到最新版再检查终端。反正我在这类奇怪问题上栽过好几回最后发现一半是版本的锅一半是终端的锅。6. 最后分享几个我常用的习惯文章最后说点只有长期使用才会总结出来的小习惯。第一进度条的描述文字尽量带上动态信息。内层desc写成f文件{file_id}比写死的“内层”有用得多。一旦出问题你能直接从进度条上判断当前卡在哪个具体任务上省去一堆debug日志。第二进度条用完随手close。虽然tqdm在对象被回收时也能自动关闭但显式close可以确保终端光标状态被正确恢复避免脚本结束之后终端残留异常光标行为。第三把tqdm封装成统一工具函数别到处散落裸调用。比如在项目里封装一个get_progress(total, desc, position)函数全局换样式时只改一处维护成本大幅降低。这篇文章里的代码我在Python 3.10和tqdm 4.66环境下完整跑过命令行直接运行没问题。如果你复现时表现不一样优先检查两个变量tqdm版本和终端类型。这两个因素对进度条显示的影响远大于代码本身。