KernelRanch
返回文章列表

tqdm 4.70:把进度条写对,避开三种常见误用

2026-11-10 #Python

跑一个几万次的循环,中间没有任何反馈,既看不出还要多久,也分不清是慢还是卡死。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.py4.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 做文件读写进度。这些都没有在本机验证。
  • 耗时数字是单次测量,未做多轮取中位数。