前言
长循环没有反馈,是脚本体验差的头号原因:跑了几分钟,光标一动不动,你分不清它在算、在等 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="计算平方"):
passtotal是「预期迭代次数」,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或改走日志,而不是把进度条硬塞进文件。