
在Python里做循环处理tqdm应该是我见过最顺手的一个进度条库了。几百上千次循环它一条动态进度条就能把进度、耗时、速度全摆在你面前。但你有没有遇到过这种情况外层循环几千次内层循环几百次用单条进度条时内层跑得太快进度条“唰”地一下冲到底外层到底进行到哪一步了反而完全没人知道。这种场景就需要把进度条做成双层外层显示主流程的整体进度内层显示当前阶段内部拆出来的小任务进度。这篇文章就以我自己实际项目里的用法为例把tqdm双层嵌套进度条的正确姿势——包括两种核心写法、样式调优和踩坑经验——一次讲清楚。不管你是Python入门没多久的新手还是已经在写训练循环、批处理脚本的老手这套思路都能直接拿去用。1. 为什么单层进度条不够用双层进度条解决的问题先说一个我踩过的真实场景。当时要处理一批数据100个文件夹每个文件夹里又有100个文件需要对每个文件做解析和清洗。一开始我图省事只用一个tqdm包住内层循环。from tqdm import tqdm import time for folder_idx in range(100): time.sleep(0.05) for file_idx in tqdm(range(100), descf处理文件): time.sleep(0.01)跑起来之后问题立刻暴露进度条永远在刷“处理文件”当前是第几个文件夹根本看不到。100个文件夹还好我能靠大概时间猜如果是3000个文件夹每次程序中断重启我都不知道自己处理到哪了。这种“局部看得很清楚全局完全失控”的感觉正是单层进度条的天花板。双层进度条的核心价值是信息分层。最外层进度条只回答一个问题整个任务完成了百分之多少。内层进度条只回答另一个问题当前这个阶段内部又完成了百分之多少。两层各司其职互不干扰。这跟我们平时下载文件时看到的界面是同一个道理——主任务进度条下面还有一条子进度条二者分别表达“下载总量”和“当前分片”谁也不会拿局部信息去冒充整体信息。什么样的任务适合用双层进度条我总结下来主要有三类嵌套循环结构外层遍历大目录或大列表内层对每个元素再做细粒度操作比如文件夹里嵌套文件、批次里嵌套样本。流程阶段拆分一个流程分为下载、校验、导入三个阶段阶段维度用外层阶段内部条目维度用内层。训练任务epoch用外层batch用内层这样训练到第几个epoch、当前epoch里处理了多少batch一眼就能同时看到。反过来如果内层循环只有三五次或者内层操作本身就很快那就没必要硬上双层。多一条进度条就多一份终端渲染开销而且看起来也乱。做技术选型最忌讳的就是拿着高级方案去解决根本不存在的问题。判断标准很简单单条进度条能不能同时回答“整体进度”和“当前细节”这两个问题能回答就别折腾回答不了再上双层。2. 两种核心写法嵌套进度条与手动刷新进度条实现双层的方案网上各有各的说法但核心其实就两条路一种是直接用两个tqdm嵌套在循环里让tqdm自动管理进度条的显示位置另一种是提前创建两个tqdm实例手动控制它们的update和重置。两条路各有适用场景下面拆开讲。2.1 写法一最简单的嵌套写法先用最直观的方式实现一遍内层循环直接用tqdm包住外层循环也用tqdm包住。from tqdm import tqdm import time outer tqdm(range(10), desc外层任务, position0) for i in outer: # 每个外层任务内部再拆 100 个小步骤 inner tqdm(range(100), descf内层任务[{i}], position1, leaveFalse) for j in inner: time.sleep(0.01)这段代码跑起来后终端里会同时出现两行上面一行是内层任务的进度条下面一行是外层任务的进度条。随着外层i的增加内层描述里的编号也在变你会很清楚地看到当前正在做的具体子任务是什么。这里有几个参数必须解释清楚不然后面百分之百踩坑。position参数指定进度条在第几行显示。数值越大的进度条越靠上position0固定在终端最底部。内外两层必须指定不同的position否则两条进度条会在同一行打架输出来回跳看着像终端抽风。外层用0内层用1这是最常用的组合。leaveFalse是内层进度条的生命周期开关。它表示内层进度条完成后自动从终端上消失不占位置。这个参数极其重要——如果把它写成leaveTrue每跑完一个外层任务终端上就会残留一条已经完成的内层进度条跑完10个外层任务终端上会堆出10条僵尸进度条。那种画面谁看谁头疼。desc写成动态字符串是我特别建议的一个习惯。内层条的目的不只是显示细节进度还要显示“我正在做哪件事”。把外层索引或当前处理对象的关键信息拼进desc信息量直接翻倍。比如我在批处理图片时desc会写成f处理图片 {img_name}跑起来一眼就能定位到当前卡在哪张图上。2.2 写法二手动刷新内层条嵌套写法在“内层循环只有一层”的时候很清爽但一旦内层循环里还有条件分支、异常重试、或者是多个小循环拼出来的“伪内层”嵌套写法的控制力就不够了。这时候我一般用手动刷新方案先把两个进度条创建出来然后在程序里手动调用update。手动刷新的核心逻辑是外层进度条负责管整体计数内层进度条负责管当前阶段的计数。当一个阶段结束就把内层进度条重置再继续下一阶段。from tqdm import tqdm import time outer_total 10 inner_total 100 outer tqdm(totalouter_total, desc外层任务, position0) inner tqdm(totalinner_total, desc内层任务, position1, leaveFalse) for i in range(outer_total): inner.set_description(f内层任务[{i}]) for j in range(inner_total): # 这里可以做任意逻辑 time.sleep(0.002) inner.update(1) # 当前阶段结束内层条清零外层条前进一格 inner.reset() outer.update(1)这个方案和嵌套写法最大的区别在于内层进度条并不是跟着for循环自动更新的而是我主动在循环体里调用update(1)。代价是多写一行代码但换来的控制力非常关键。举一个实际的例子假设内层任务需要从数据库读取数据、做异常重试、过滤非法值然后再批量写入。整个流程不是一个单纯的for range能表达的而是一个带有复杂逻辑的循环。如果用嵌套写法你需要把整个复杂循环体塞进一个生成器里代码结构会很拧巴。而手动刷新方案里你只管在合适的时机调用update(1)其余逻辑想怎么写就怎么写。另一个好处是可以动态调整内层总数。比如第一个阶段要处理80条第二个阶段要处理120条内层tqdm的重置函数reset(total120)可以直接把总数改掉进度条立刻按新总数显示。这在嵌套写法里做起来会很别扭因为你得重建整个内层进度条对象。注意reset()只重置进度计数不会改变之前设置过的desc和leave等属性。所以阶段切换时想改描述需要另外调用set_description()。2.3 两种写法的选择逻辑我自己的选择标准是这样的内层逻辑是标准for循环且内层循环体不需要额外控制就用嵌套写法代码短、直观内层逻辑复杂或者在同一个外层任务里内层要分好几个子阶段完成就用手动刷新写法控制力优先。还有一个值得留意的细节手动刷新方案里两个进度条对象创建顺序和position设置要一致。position0是外层position1是内层写在同一个函数开头后面就不再变化。有人喜欢在for循环内部临时创建内层进度条那跟嵌套写法就没区别了而且每次循环都会new一个对象开销比复用对象大很多。3. 美观度实战只改三个参数进度条立刻变高级双层进度条能正确显示只是第一步。大多数人电脑上的进度条之所以难看是因为用了默认输出格式——一堆百分比、耗时、速度挤在一行里像日志不像进度条。我自己常用的优化手段其实就三个调整进度条长度、打开自适应宽度、设置合理的速率显示。做好这三样进度条的颜值能提升一个档次。3.1 用bar_format控制显示样式还是先看代码。我建议在创建进度条时显式传一个bar_format不要用默认格式。from tqdm import tqdm import time bar_format {l_bar}{bar:25}{r_bar}{bar:-10b} outer tqdm(total10, desc外层任务, position0, bar_formatbar_format) inner tqdm(total100, desc内层任务, position1, leaveFalse, bar_formatbar_format) for i in range(10): for j in range(100): time.sleep(0.005) inner.update(1) inner.reset() outer.update(1){bar:25}表示进度条的填充区域固定占25个字符宽。这个宽度配合终端大小做微调通常是80到120之间视觉最舒服。太窄了看不清进度粒度太宽了在宽屏终端里又显得松散。{r_bar}是默认的右侧信息板块包括百分比、已用时间和预估剩余时间。大多数人觉得进度条杂乱主要原因就是这里信息太多。如果你只关心百分比可以直接替换成{percentage:3.0f}%如果你还想看到进度数字保留{n_fmt}/{total_fmt}也行。我会根据场景在r_bar里做减法该砍的信息一律砍掉。3.2 打开dynamic_ncols避免换行错位双层进度条最容易翻车的场景之一是终端窗口宽度发生变化。比如你一边跑脚本一边调整终端大小窄了之后进度条换行两行进度条瞬间错位内层描述甚至直接盖到外层上面。解决办法很简单创建tqdm时加上dynamic_ncolsTrue。outer tqdm(total10, desc外层任务, position0, dynamic_ncolsTrue) inner tqdm(total100, desc内层任务, position1, leaveFalse, dynamic_ncolsTrue)这个参数的意思是每次刷新进度条时自动检测终端宽度动态调整进度条实际占用的字符数。实测下来打开之后至少不会再出现进度条溢出换行的情况。代价是每次刷新多了一点检测终端的系统调用但对于人类肉眼能感知的刷新频率来说这点开销完全可以忽略。3.3 速率单位与单位换算unit和unit_scale还有一个容易忽略的美观细节速率单位。默认情况下tqdm显示的是it/s也就是每秒迭代次数。这个单位本身没什么问题但当你每秒钟处理数万条记录时35000.00 it/s这种显示就很蠢。我习惯加上unit和unit_scale来调整。from tqdm import tqdm # 处理图片数量 outer tqdm(range(1000), desc外部目录, unit张, unit_scaleTrue, position0) # 批量写入数据 inner tqdm(total50000, desc写入行数, unit行, unit_scaleTrue, position1, leaveFalse)unit张会把进度条里默认的“it”替换成“张”比如进度显示会变成“356/1000张”unit_scaleTrue则会对较大数值自动做单位换算比如50000会显示为50.0k。两个参数配合起来进度条的可读性会高很多尤其适合给非技术同事演示脚本运行情况时使用。3.4 用smoothing平滑速率波动还有一个小参数我经常用smoothing。默认情况下tqdm显示的速率是历史平均速率某一步特别慢或者特别快平均速率波动不大看起来一条直线。但如果你的任务每步耗时差异很大平均速率会显得很不敏感。此时可以设置smoothing0.3让速率更偏向最近几步的表现。pbar tqdm(range(1000), desc混合任务, smoothing0.3)这个参数比较玄学数值在0到1之间0表示纯平均值1表示只看最近一步。我一般从0.3起步根据实际曲线做微调。速度波动明显的任务进度条会显得更“活”也更真实。3.5 美观度的最终版示例把这些参数合在一起一个我实际用着很顺手的双层进度条配置长这样from tqdm import tqdm import time bar_format {desc}: {bar:30} {percentage:3.0f}% [{n_fmt}/{total_fmt}] {rate_fmt} outer tqdm( total5, desc全部数据, position0, bar_formatbar_format, dynamic_ncolsTrue, unit批, unit_scaleFalse, smoothing0.3 ) inner tqdm( total200, desc当前批次, position1, leaveFalse, bar_formatbar_format, dynamic_ncolsTrue, unit条, unit_scaleTrue, smoothing0.3 ) for i in range(5): inner.set_description(f批次{i1}) for j in range(200): time.sleep(0.002) inner.update(1) inner.reset() outer.update(1)这个配置显示出来终端上是两条结构一致的进度条上面的描述随时切换“批次1、批次2……”下面始终稳定显示整体进度不会出现多条进度条样式五花八门的问题。风格统一这件事比大多数人想象的更重要。4. 实战案例批处理与训练循环中的完整模板样式讲完了下面把双层进度条直接挪到几个真实场景里跑一遍。每个场景我都在自己的电脑上实测过可以直接复制改一改就用。4.1 场景一批量图片压缩假设你有一个图片库几千张照片要逐张压缩并另存。外层是图片文件列表内层是每张图片内部按行处理像素块。这里的内层用像素分块来模拟符合“外层文件、内层子任务”的结构。from pathlib import Path from tqdm import tqdm from PIL import Image import time src_dir Path(images) out_dir Path(compressed) out_dir.mkdir(exist_okTrue) files list(src_dir.glob(*.jpg)) outer tqdm(totallen(files), desc图片压缩, position0, unit张, bar_format{desc}: {bar:25} {percentage:3.0f}% {n_fmt}/{total_fmt}) inner tqdm(total100, desc处理中, position1, leaveFalse, unit块, bar_format{desc}: {bar:25} {percentage:3.0f}%) for img_path in files: inner.set_description(f处理中 {img_path.name}) with Image.open(img_path) as img: img img.convert(RGB) # 模拟对图片分块处理的过程 for block in range(100): time.sleep(0.001) inner.update(1) img.save(out_dir / img_path.name, quality80) inner.reset() outer.update(1)这个用法有几个细节值得注意外层遍历文件列表时我提前把files转成了列表因为Path.rglob本身是生成器不转列表就拿不到总长度tqdm的total无从谈起内层描述里带上文件名压缩过程中一旦发现某张图异常能马上定位到具体文件。我在一个接近6000张图片的目录上跑过两条进度条配合得很稳定。4.2 场景二深度学习训练epoch加step训练场景里双层进度条几乎是标配。外层看epoch内层看batch。下面这段是伪代码但结构上完全可以直接迁移到PyTorch或Keras的训练循环中。from tqdm import tqdm import torch epochs 30 dataloader torch.utils.data.DataLoader(dataset, batch_size32) for epoch in range(epochs): epoch_pbar tqdm( totallen(dataloader), descfEpoch {epoch1:03d}/{epochs:03d}, position0, bar_format{desc} | {bar:20} | {percentage:3.0f}% | {n_fmt}/{total_fmt} | loss{postfix} ) epoch_loss 0.0 for batch_idx, (x, y) in enumerate(dataloader): # 这里省略前向、反向、优化的代码 loss 0.123 epoch_loss loss epoch_pbar.set_postfix(lossf{loss:.4f}) epoch_pbar.update(1) epoch_pbar.close()这里我只用了外层进度条展示每个epoch的进度并没有强行加内层。为什么因为如果你的batch很短内层加出来的意义不大如果你的batch很长可以用同样方式再加一条position1的内层进度条展示更细粒度的子流程。双层不等于非得用满两层该用几层用几层这是经验问题。在这个示例里set_postfix是一个特别值得用的方法。它可以把动态指标比如当前loss追加到进度条右侧显示训练过程中不需要额外print所有信息都在进度条上。这个技巧在偷看训练状态时非常关键可以避免满屏日志刷得乱七八糟。4.3 场景三并行下载加校验还有一个我最近被频繁问到的场景同时下载多个文件下载完成后再逐个校验。这个场景不完全是嵌套循环更像“整体进度”加“当前文件内部状态”的并列展示。import hashlib from tqdm import tqdm import time download_list [file_a.zip, file_b.zip, file_c.zip] outer tqdm(totallen(download_list), desc下载任务, position0, unit个) inner tqdm(total100, desc正在下载 file_a.zip, position1, leaveFalse, unit%) for filename in download_list: inner.set_description(f正在下载 {filename}) # 模拟分块下载 for percent in range(100): time.sleep(0.01) inner.update(1) # 模拟校验 time.sleep(0.3) inner.reset() outer.update(1)这种场景的内层语义已经从“子任务列表”变成了“当前文件的进度百分比”。百分比本身就是总数100内层update逻辑就是每次更新一个点。这种用法在下载、上传、md5计算等场景很常见本质就是用一个条跟踪整体一个条跟踪当前正在做的那件事。5. 常见问题与避坑技巧实录用tqdm做双层进度条实现本身不难难的是在真实环境中稳定运行。我把自己踩过、以及帮别人排查过的坑按高频率整理出来做成一个速查表。5.1 常见问题速查表症状根本原因解决办法两条进度条在同一行重叠疯狂闪动没有给内外层指定不同的position分别设置position0和position1内层进度条跑完不消失残留了好几行leave默认或误设为True内层进度条设置leaveFalse外层进度条卡住不动忘了调用outer.update(1)只更新了内层每个阶段结束时明确调用外层update进度条换行错位内层文字盖住外层终端宽度变化导致渲染宽度溢出开启dynamic_ncolsTrue控制台里进度条和print输出乱成一团print和tqdm同时写同一个终端使用tqdm.write替代print进度条正常显示但速率数值一直是0总任务耗时过短平均速率显示不敏感设置smoothing0.3或者用bar_format去掉速率在Jupyter Notebook里进度条不显示或乱跳默认tqdm和IPython的widget不兼容使用from tqdm.notebook import tqdm进度条在Windows PowerShell里出现方块乱码终端不支持默认的Unicode字符设置asciiTrue或用新的Windows Terminal5.2 排查经验一进度条闪烁与position冲突两条进度条闪烁是最常见的问题通常发生在同一个终端里混用了嵌套写法和手动刷新写法。比如外层用了手动的progress内层却new了一个新的tqdm从头开始。两个tqdm都试图用默认position值而默认值在没有显式指定时会由tqdm内部统一分配。一旦分配方案和你预期不一致就会出现同一行互相覆盖的情况。我的习惯很固定整个项目中所有进度条都显式指定position绝不让tqdm自己猜。就算只用了单层进度条我也会习惯性写上position0为后续改为双层留出余地。这个习惯救过我很多次。5.3 排查经验二日志输出破坏进度条布局另一个高频问题是想在循环里看日志。很多人直接print一个状态结果终端上出现了大量的print文本把进度条挤得乱七八糟。表面看是排版问题实际上是因为print写标准输出时不会考虑进度条正在控制的光标位置。解决方案就是使用tqdm.write()。这个函数会先清理进度条占用的行输出你的消息再把进度条重新画到终端上。我通常这样组织代码from tqdm import tqdm import time outer tqdm(total3, desc处理中, position0) inner tqdm(total100, desc子任务, position1, leaveFalse) for i in range(3): tqdm.write(f开始处理第 {i1} 个任务) for j in range(100): time.sleep(0.005) inner.update(1) inner.reset() outer.update(1)这里的日志会整整齐齐地打印在进度条上方进度条本身不受影响。如果坚持用print轻则日志穿插在进度条中间重则进度条闪烁甚至消失尤其是当循环长时间运行时更明显。5.4 排查经验三Jupyter Notebook环境下适配如果你是在Jupyter Notebook里跑双层进度条直接用from tqdm import tqdm会碰到兼容性问题。IPython的输出系统是基于HTML渲染的widget跟终端文本刷新机制完全不同。正确做法是分开导入# 终端里用这个 from tqdm import tqdm # Jupyter Notebook里用这个 from tqdm.notebook import tqdm如果想写一段代码终端和Notebook都能跑可以统一换成from tqdm.auto import tqdmtqdm.auto会智能检测当前环境自动选择底层实现。我现在的脚本开头基本都写from tqdm.auto import tqdm好处是代码换环境不用动坏处几乎可以忽略。稍微提醒一下Notebook里的双条刷新不能用position控制因为widget布局的方式和终端是不同的这一点需要有个心理预期。5.5 避开一个容易忽略的坑进度条宽度不一致最后分享一个有强迫症的人会在意的细节两条进度条宽度不一致。出现这个问题的原因通常是两条进度条使用了不同的bar_format或者同样的bar_format里宽度参数不同。比如外层写了{bar:30}内层忘了写宽度默认自适应两条条宽度就有细微差别看起来非常难受。我的建议是把bar_format定义成一个共用常量内外都用同一个格式。这样不仅宽度一致而且整体风格统一。之前有个同事把所有进度条都用了默认样式结果显示出来有的有速率有的没速率有的有剩余时间有的没有看着像三个不同程序拼在一起。统一bar_format之后问题立刻消失。6. 关于tqdm源码与刷新机制的一点理解最后补一小段距离实操比较近的底层理解帮大家真正“吃透”双层进度条为什么能同时显示。tqdm的原理并不神秘。它本质上维护了一个计数器n和总数total每次调用update(delta)时把n加上delta然后计算百分比、速率、剩余时间再往终端里写入一段带有控制符的文本。这段文本会先把之前占用的行清掉再用新的内容重新绘制。之所以两个进度条能同时存在是因为每条tqdm有自己的position占位tqdm内部通过维护一个全局的行分配表让每个位置明确的进度条互不覆盖。当你把外层固定到position0内层固定到position1两条进度条就会被安排到终端的不同行各自刷新各自的行。这种“多行独立刷新”的机制是所有双层乃至多层进度条能够成立的基础。理解了这一点你就能推断出很多实战问题为什么内层leave设为False时才不会残留因为进度条完成后默认会重新绘制一次把该行的内容抹掉为什么不给position会自动错乱因为tqdm内部对未指定位置的对象会用“当前所有进度条中最小空闲行号”这种策略一旦有进度条提前退出新的进度条会钻进旧位置视觉上就会跳来跳去。这段底层机制是我把tqdm用了很久之后才认真翻源码得来的。很多时候我们觉得某个库不好用、“老是出小毛病”其实是因为只停留在“会调用API”的层面没有理解它到底怎么工作。对tqdm这种轻量级库花二十分钟读一遍源码初期的刷新逻辑以后再遇到什么奇怪问题都能自行推理。7. 我个人实践后的体会双层进度条这个需求看起来小其实非常能锻炼工程直觉。我刚接触tqdm时只会用最基础的for i in tqdm(range(n))遇到多层循环要么只看内层要么干脆用print打点。直到有一次跑一个需要十几分钟的数据处理任务因为进度信息不全中途中断后完全不知道断在哪里才认真研究双层方案。现在我的代码习惯已经固定成一套流程先判断任务是否存在实际的多层维度然后确定外层和内层各自的总数是什么再手动指定两个position最后统一bar_format。整个过程一分钟左右就能想清楚但带来的好处是每次跑长时间任务时哪怕人不在屏幕前回来后扫一眼进度条就能立刻了解程序状态。最后再分享一个小技巧如果你的任务数据量特别大可以在外层进度条完成之后不关闭而是通过修改desc把它变成一行总结文字比如“全部处理完成耗时12分08秒”。然后关闭内层进度条。这样脚本结束后终端上留下的不是一堆残留进度条而是一行清晰的执行摘要。这个做法在自动化运维脚本和定时任务里尤其实用给人看的时候也特别体面。把双层进度条用顺了之后你会发现它不只是“好看”而已。它本质上是一种信息展示的克制全局信息用一条局部信息用一条从不混淆。能做到这一点的代码读起来、跑起来、用起来都会舒服很多。希望这篇实战分享能帮你少走几步弯路真正把tqdm用出价值来。