☰
Python进度条神器tqdm使用实例详解
2026/10/12 14:02:31 网站建设 项目流程

前言


长循环没有反馈,是脚本体验差的头号原因:跑了几分钟,光标一动不动,你分不清它在算、在等 I/O,还是早就卡死了。tqdm这个第三方库就是来解决这件事的——把它套在可迭代对象外面,终端里立刻出现一条实时刷新的进度条。


先纠正两个常见误解。第一,以为tqdm是标准库,import tqdm就能用——它是第三方库,需要先pip install tqdm。第二,以为进度条是「一行一行往下打印」的——恰恰相反,它的核心动作是往输出流写一个回车符再重绘同一行,所以它不是「打印一行」,而是「原地刷新」。理解这一点,才能解释为什么把输出重定向到文件时,文件里会塞满大量被回车覆盖的中间状态。


本文讲清tqdm的几种基本用法(包裹可迭代对象、手动update、total与desc、嵌套进度条),并把它「往 stderr 写\r」的工作原理说明白。示例按tqdm4.x 的接口书写,具体参数以该库官方文档为准。运行环境需要 Python 3(Python 2.7 已于 2020-01-01 停止维护,本文不适用)。


一、安装与最小用法


pip install tqdm

用tqdm包住任何可迭代对象即可:


# 适用于 Python 3.7+;需先 pip install tqdm
from tqdm import tqdm
import time

for i in tqdm(range(100)):
time.sleep(0.01)

如果可迭代对象有__len__,tqdm能自己算出总数,于是进度条、百分比、剩余时间一应俱全。要是对象没有长度(比如生成器、文件句柄),就得自己告诉它总数,否则只会显示「已处理 N 个」,没有百分比。


# 适用于 Python 3.7+;需先 pip install tqdm
from tqdm import tqdm


def gen(n):
for i in range(n):
yield i * i


# 生成器没有 len(),用 total 显式给出总数
for value in tqdm(gen(50), total=50, desc="计算平方"):
pass

total是「预期迭代次数」,desc是进度条前面的文字说明。两者都是可选参数,给与不给只影响显示,不影响迭代本身。


二、手动控制:update 与 close


当循环结构不是「一个个消费可迭代对象」时(比如按批次处理、按字节数推进),就用手动模式:不传iterable,自己调update()。


# 适用于 Python 3.7+;需先 pip install tqdm
from tqdm import tqdm
import time

pbar = tqdm(total=1000, desc="下载", unit="KB")
try:
for _ in range(10):
time.sleep(0.05)
pbar.update(100) # 每次推进 100 个单位
finally:
pbar.close() # 循环结束或中途异常都要收尾

几个要点:



  • total与update的单位要一致。上面total=1000配每次update(100),十次刚好走满。

  • unit只是显示用的单位名(默认是it),不参与计算。

  • 没有用with或for包裹时,必须自己close(),否则进度条可能停在半路、或者不显示最终 100% 的状态。

  • 想要「异常时也不留半条进度条」,用with语句,tqdm对象支持上下文管理器协议:


# 适用于 Python 3.7+;需先 pip install tqdm
from tqdm import tqdm
import time

with tqdm(total=100, desc="处理") as pbar:
for _ in range(10):
time.sleep(0.02)
pbar.update(10)

也可以给tqdm传initial=已完成的量,用于「接上一次中断的进度继续」。循环过程中还能动态改描述:


# 适用于 Python 3.7+;需先 pip install tqdm
from tqdm import tqdm

with tqdm(total=3) 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 tqdm
from tqdm import tqdm
import time

for epoch in tqdm(range(3), desc="epoch", position=0):
for step in tqdm(range(20), desc="step", position=1, leave=False):
time.sleep(0.005)



参数作用嵌套时的建议



total预期总数每层都写清楚,否则没有百分比

desc前缀文字每层给不同文字,便于区分

position占第几行(从 0 开始)外层 0、内层 1,逐层递增

leave结束后是否保留进度条内层设False,避免刷屏

file输出到哪个流默认sys.stderr,多进程时需分别指定



官方文档在讲嵌套时提到,应优先尝试让它自动判断位置;只有在确实需要手动控制(例如多进程场景)时才显式指定position=n,其中n=0是最外层。leave=False表示这一层完成后把进度条从屏幕上擦掉,内层用它能让输出干净很多。


tqdm还提供了trange,它是tqdm(range(...))的简写:


# 适用于 Python 3.7+;需先 pip install tqdm
from tqdm import trange
import time

for 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 tqdm
from tqdm import tqdm
import sys
import time

# 把进度条写进一个文件,观察 \r 重绘在「非终端」上的后果
with open("progress.log", "w", encoding="utf-8") as f:
for i in tqdm(range(5), file=f, mininterval=0.0, miniters=1):
time.sleep(0.05)

用一个纯粹的字符串来模拟这种「重绘」是什么感觉:


# 适用于 Python 3.8+
import sys
import 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 tqdm
from tqdm import tqdm
import time

with tqdm(total=3) 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的中间状态,文件被大量残留撑大且难以阅读。 ✅ 明确让进度条在终端显示、日志走别的流;或判断输出不是终端时用disable=True关掉进度条。


坑点 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)但total=100—— 进度直接冲到 10000%,显示乱了。 ✅update的步进单位与total的单位保持一致;不确定就都用「个数」。


坑点 6:嵌套时不设position或不关内层leave。❌ 两层都用默认值 —— 两个进度条抢同一行,画面错乱;内层结束后留下一屏历史条。 ✅ 外层position=0,内层position=1, leave=False;官方建议先试自动定位,不行再手动指定。


坑点 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(), total=N)显式给总数

手动推进pbar.update(n)记得close()

改前缀文字pbar.set_description(text)只影响显示

嵌套position=0/1、内层leave=False先试自动定位

打印日志不打架pbar.write(msg)会先让开进度条

输出到别处file=参数默认是sys.stderr

关掉进度条disable=True非终端环境推荐



tqdm的全部魔法其实就一句话:往流里写\r回到行首,再重绘同一行。默认写sys.stderr、默认带节流,都是为了「既看得见、又不干扰结果」。一旦离开终端(重定向到文件、被 CI 收集日志),\r就失去意义、只剩残留,这时候正确的做法是disable或改走日志,而不是把进度条硬塞进文件。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询