tqdm 4.70:把进度条写对,避开三种常见误用
跑一个几万次的循环,中间没有任何反馈,既看不出还要多久,也分不清是慢还是卡死。tqdm 把可迭代对象包一层,就地刷新一行进度、速率和剩余时间,代码改动只有 for x in tqdm(items)。它的开销主要在刷新频率上:默认每 0.1 秒重绘一次,几百万次的紧凑循环里这个开销会显现出来,可以用 mininterval/miniters 调;如果输出目标是日志文件而不是终端,进度条本身没什么意义,disable 掉更干净。
安装与实际版本
python -m venv D:\ai\cache\temp\py-tqdm
D:\ai\cache\temp\py-tqdm\Scripts\python.exe -m pip install tqdm
Name: tqdm
Version: 4.70.1
Location: D:\ai\cache\temp\py-tqdm\Lib\site-packages
最小可运行示例
进度条靠 \r 回到行首重绘,直接看终端截图只能看到最后一帧。为了看到"真实写入了什么",把 file 指到 StringIO,再按 \r 切分:
import io
import time
from tqdm import tqdm
buf = io.StringIO()
for _ in tqdm(range(3), file=buf, ncols=40, desc="加载", unit="行"):
time.sleep(0.01)
raw = buf.getvalue()
print("原始输出里的回车符个数:", raw.count("\r"))
print("最后一帧:", [f for f in raw.split("\r") if f.strip()][-1].strip())
原始输出里的回车符个数: 2
最后一帧: 加载: 100%|#| 3/3 [00:00<00:00, 96.07行/
3 次迭代只写了 2 个回车,因为默认 mininterval=0.1 秒,10 毫秒一轮的循环大部分刷新被合并了。ncols=40 把整行限制在 40 列内,所以条形图被压成一个 #,尾部也被截断。
手动模式适合"总量边跑边知道"的场景,先 update(),中途改 total 再 refresh():
buf3 = io.StringIO()
with tqdm(file=buf3, ncols=40, desc="扫描", unit="文件") as bar:
for _ in range(7):
bar.update(1)
bar.total = 20
bar.refresh()
for _ in range(13):
bar.update(1)
print("最后一帧:", [f for f in buf3.getvalue().split("\r") if f.strip()][-1].strip())
最后一帧: 扫描: 100%|#| 20/20 [00:00<00:00, 838.18
常用 API 与参数
| 名称 | 作用 | 要点 |
|---|---|---|
tqdm(iterable) | 包裹可迭代对象 | 有 __len__ 时自动取 total |
desc= | 前缀文字 | 显示在百分比之前 |
total= | 总步数 | 未知总量时给 None,只显示计数 |
unit= / unit_scale= | 单位 | unit="B" + unit_scale=True 显示 KB/MB |
ncols= | 整行宽度 | 0 表示只留统计数字不要条形 |
mininterval= / miniters= | 刷新频率 | 默认 0.1 秒;紧凑循环里调大能省开销 |
file= | 输出去向 | 默认 sys.stderr,可指 StringIO 便于测试 |
disable= | 关闭 | True 完全不写;None 时非 TTY 自动关闭 |
leave= / position= | 结束后是否保留/行位置 | 嵌套进度条靠 position 分层 |
tqdm.write() | 不打断进度条地打印 | 比 print 安全;bar.update(0) 强制重绘 |
三种误用
第一种是把进度条写进日志。tqdm 默认的 file 是 sys.stderr,而 \r 是给终端用的控制字符;重定向到文件之后,每一次刷新都变成文件里的一段"回车加新内容",用编辑器打开是一行乱码或者一行被覆盖得只剩尾巴。要判断该不该开,用 disable=None:非 TTY 自动关闭,写文件时不产生任何输出。
disable=True 时写入了 0 个字符
第二种是在循环里 print。print 写 stdout,进度条写 stderr,两者会交错,进度条被撕成几段。用 tqdm.write 走同一把锁:
输出中是否出现独立成行的提示: True
总行数(按换行切): 4
tqdm.write 会先把当前进度条清掉、打印一行、再重绘。这也是为什么它比 print 慢一点,换来的是不断行。
第三种是包生成器不给 total。没有总量时 tqdm 不知道还剩多少,只能显示计数和速率:
最后一帧: 无 total: 5it [00:00, 189.36it/s]
没有百分比、没有剩余时间。能从 len() 拿到的就传进去,实在拿不到就用 total= 给一个估计值,用户对"大概还剩多少"的容忍度远高于一片空白。
速率与嵌套
mininterval 会影响"看到什么",也会影响测出来的耗时。50 次、每次 2 毫秒的循环:
最后一帧: 限速: 100%|######| 50/50 [00:00<00:00, 418.98it/s]
实际耗时: 0.119s
0.119 秒接近 50×2ms=0.1 秒,说明刷新开销在这组参数下可以忽略。循环体更空、迭代次数上百万时,mininterval 的作用才显出来。
嵌套进度条要靠 position 显式分层,否则两条会抢同一行:
outer = tqdm(range(2), file=buf7, ncols=40, desc="批次", position=0)
for _ in outer:
for _ in tqdm(range(3), file=buf7, ncols=40, desc="内层", position=1, leave=False):
time.sleep(0.002)
嵌套时帧数: 6
6 是外层的 2 帧加内层的 4 帧(内层 leave=False,结束后不保留)。帧数不等于行数,因为都被 \r 覆盖在同一片区域里。
小结
tqdm 的参数分成两组:一组决定显示什么(desc、unit、total、ncols),一组决定多久画一次(mininterval、miniters)。前者关系到可读性,后者关系到开销,紧凑循环里把刷新间隔调大是唯一有效的优化手段。
三种误用的根因是同一个:进度条假设目标是终端。写进文件、混进 print、在 CI 里滚动日志,都会让 \r 变成噪音。disable=None 或 disable=not sys.stderr.isatty() 能自动处理这件事,比自己判断环境变量可靠。
数据来源与检索时间
检索时间 2026-11-10。版本、帧数与耗时为本机实测。
| 来源 | URL | 对应数据 |
|---|---|---|
| PyPI《tqdm》项目页 | https://pypi.org/project/tqdm/ | 包名、安装方式与当前版本 4.70.1 |
| tqdm 官方文档《tqdm.std》 | https://tqdm.github.io/docs/tqdm/ | tqdm() 的完整参数表:desc、total、leave、file(默认 sys.stderr)、ncols、mininterval(默认 0.1s)、miniters、unit、unit_scale、unit_divisor、disable(None 时非 TTY 关闭)、position、postfix、bar_format;update()/close()/refresh() 语义;write() 的 "without overlap with bars" 说明 |
| 本机运行记录 | D:\ai\cache\temp\work\tqdm_demo.py | 4.70.1;3 次迭代的 2 个回车与最后一帧;无 total 的生成器帧;manual 模式改 total;tqdm.write 的行数;disable=True 的 0 字符;50 次限速循环的帧与 0.119s;嵌套的 6 帧 |
核验说明
- 虚拟环境
D:\ai\cache\temp\py-tqdm,Python 3.11.9,pip install tqdm装到 4.70.1。 - 真跑的示例:
tqdm(range(3))配StringIO抓帧、无total的生成器、manual模式改total、tqdm.write与普通输出的行数对比、disable=True、mininterval=1.0的 50 次限速循环、嵌套position的帧数。 - 所有输出都是通过管道捕获的,不是真实终端,因此看到的是
\r覆盖后的原始字节切片,不是动画效果;条形图被ncols压缩成一个#也是这个原因。 - 没测的部分:
tqdm.pandas()的progress_apply、tqdm.notebook、多线程/多进程下的set_lock、bar_format自定义模板、wrapattr做文件读写进度。这些都没有在本机验证。 - 耗时数字是单次测量,未做多轮取中位数。