なぜプログレスバーを自作するのか
Pythonでプログレスバーといえば tqdm が定番ですが、自作することにもメリットがあります。
- 外部依存をゼロにできる(
pip install不要) - ターミナル制御の仕組みを理解できる
- 表示フォーマットを自由にカスタマイズできる
この記事では、キャリッジリターンの基礎から始めて、ETA表示・ANSIカラー・マルチバーまで段階的にプログレスバーを実装していきます。
キャリッジリターンの基本
プログレスバーの核心は、ターミナルの同じ行を繰り返し上書きすることです。これには キャリッジリターン \r を使います。
\r はカーソルを行の先頭に戻す制御文字です。改行せずに出力すれば、前の内容を上書きできます。
import time
for i in range(101):
print(f"\r処理中... {i}%", end="", flush=True)
time.sleep(0.05)
print() # 最後に改行
ポイントは3つです。
\rを文字列の先頭に置くend=""で改行を抑制するflush=Trueでバッファを即時フラッシュする(これがないと表示が更新されないことがあります)
基本的なプログレスバー
数値だけでは味気ないので、バーを描画してみます。[████████░░░░░░░░] 50% (50/100) のような形式を目指します。
import sys
import time
def progress_bar(current, total, bar_length=30):
fraction = current / total
filled = int(bar_length * fraction)
bar = "█" * filled + "░" * (bar_length - filled)
percent = fraction * 100
sys.stdout.write(f"\r[{bar}] {percent:5.1f}% ({current}/{total})")
sys.stdout.flush()
# 使用例
total = 100
for i in range(total + 1):
progress_bar(i, total)
time.sleep(0.03)
print()
実行すると、以下のように表示が更新されていきます。
[█████████████████░░░░░░░░░░░░░] 56.0% (56/100)
sys.stdout.write を使っている理由は、print と違って余計な改行やスペースが入らないためです。
ETA(残り時間)の計算
長い処理では「あとどれくらいかかるのか」が知りたくなります。経過時間から残り時間を推定しましょう。
import sys
import time
def format_time(seconds):
"""秒数を HH:MM:SS 形式にフォーマットする"""
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
return f"{h:02d}:{m:02d}:{s:02d}"
def progress_bar_eta(current, total, start_time, bar_length=30):
fraction = current / total
filled = int(bar_length * fraction)
bar = "█" * filled + "░" * (bar_length - filled)
percent = fraction * 100
elapsed = time.time() - start_time
if current > 0:
rate = current / elapsed # 1秒あたりの処理数
remaining = (total - current) / rate
eta_str = format_time(remaining)
rate_str = f"{rate:.1f} it/s"
else:
eta_str = "--:--:--"
rate_str = "-- it/s"
sys.stdout.write(
f"\r[{bar}] {percent:5.1f}% | ETA: {eta_str} | {rate_str}"
)
sys.stdout.flush()
# 使用例
total = 200
start = time.time()
for i in range(total + 1):
progress_bar_eta(i, total, start)
time.sleep(0.02)
print()
出力例:
[███████████████░░░░░░░░░░░░░░░] 50.0% | ETA: 00:00:02 | 49.8 it/s
current == 0 のときはゼロ除算を避けるため、ETAをプレースホルダにしています。
ANSIカラー対応
ターミナルは ANSIエスケープシーケンス を使って文字に色をつけることができます。プログレスの進捗に応じてバーの色を変えてみましょう。
主なカラーコードは以下の通りです。
| コード | 色 |
|---|---|
\033[91m | 赤(明るい) |
\033[93m | 黄(明るい) |
\033[92m | 緑(明るい) |
\033[0m | リセット |
import os
import sys
import time
def get_color(fraction):
"""進捗率に応じて色を返す(赤→黄→緑)"""
if fraction < 0.33:
return "\033[91m" # 赤
elif fraction < 0.66:
return "\033[93m" # 黄
else:
return "\033[92m" # 緑
RESET = "\033[0m"
def progress_bar_color(current, total, start_time):
try:
terminal_width = os.get_terminal_size().columns
except OSError:
terminal_width = 80
# バー以外の部分の文字数を概算して、バーの長さを決定
suffix = f" {current/total*100:5.1f}% | ETA: 00:00:00 | 000.0 it/s"
bar_length = max(10, terminal_width - len(suffix) - 4) # [] と余白分
fraction = current / total
filled = int(bar_length * fraction)
color = get_color(fraction)
bar = color + "█" * filled + RESET + "░" * (bar_length - filled)
elapsed = time.time() - start_time
if current > 0:
rate = current / elapsed
remaining = (total - current) / rate
eta_str = format_time(remaining)
rate_str = f"{rate:.1f} it/s"
else:
eta_str = "--:--:--"
rate_str = "-- it/s"
percent = fraction * 100
sys.stdout.write(f"\r[{bar}] {percent:5.1f}% | ETA: {eta_str} | {rate_str}")
sys.stdout.flush()
# format_time は前のセクションと同じ
total = 150
start = time.time()
for i in range(total + 1):
progress_bar_color(i, total, start)
time.sleep(0.02)
print()
os.get_terminal_size() でターミナルの幅を取得し、バーの長さを動的に調整しています。ウィンドウをリサイズしても表示が崩れにくくなります。
マルチバーの実装
複数のタスクを同時に進める場合、バーを縦に並べて表示したくなります。これには ANSIカーソル移動 を使います。
\033[{n}A: カーソルをn行上に移動\033[{n}B: カーソルをn行下に移動
仕組みはシンプルです。複数行を出力した後、カーソルを先頭まで戻して上書きします。
import sys
import time
import random
def format_time(seconds):
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
return f"{h:02d}:{m:02d}:{s:02d}"
def get_color(fraction):
if fraction < 0.33:
return "\033[91m"
elif fraction < 0.66:
return "\033[93m"
else:
return "\033[92m"
RESET = "\033[0m"
def render_bar(label, current, total, start_time, bar_length=25):
"""1本のバーの文字列を生成して返す"""
fraction = current / total if total > 0 else 0
filled = int(bar_length * fraction)
color = get_color(fraction)
bar = color + "█" * filled + RESET + "░" * (bar_length - filled)
elapsed = time.time() - start_time
if current > 0:
rate = current / elapsed
remaining = (total - current) / rate
eta_str = format_time(remaining)
else:
eta_str = "--:--:--"
percent = fraction * 100
return f"{label}: [{bar}] {percent:5.1f}% ({current}/{total}) ETA: {eta_str}"
def multi_progress(tasks):
"""複数タスクのプログレスバーを表示する"""
n = len(tasks)
start_times = [time.time() for _ in range(n)]
progress = [0] * n
totals = [t["total"] for t in tasks]
labels = [t["label"] for t in tasks]
# 初回表示: 空行を確保
for i in range(n):
print(render_bar(labels[i], 0, totals[i], start_times[i]))
while any(progress[i] < totals[i] for i in range(n)):
# カーソルをn行上に戻す
sys.stdout.write(f"\033[{n}A")
for i in range(n):
if progress[i] < totals[i]:
# タスクごとに異なるスピードで進む
step = random.randint(1, 3)
progress[i] = min(progress[i] + step, totals[i])
line = render_bar(labels[i], progress[i], totals[i], start_times[i])
# 行末まで消去してから改行(前の表示のゴミを消す)
sys.stdout.write(f"\r{line}\033[K\n")
sys.stdout.flush()
time.sleep(0.1)
# 使用例
tasks = [
{"label": "Download ", "total": 100},
{"label": "Extract ", "total": 80},
{"label": "Install ", "total": 120},
]
multi_progress(tasks)
実行すると、ターミナル上で以下のような表示になります。

\033[K は行末までを消去するエスケープシーケンスです。バーの長さが前回より短くなった場合に、ゴミ文字が残るのを防ぎます。
まとめ:ProgressBarクラス
ここまでの要素を1つのクラスにまとめます。
import os
import sys
import time
class ProgressBar:
def __init__(self, total, label="Progress", bar_length=None, color=True):
self.total = total
self.label = label
self.color = color
self.current = 0
self.start_time = None
if bar_length is None:
try:
self.bar_length = max(10, os.get_terminal_size().columns - 60)
except OSError:
self.bar_length = 30
else:
self.bar_length = bar_length
def _get_color(self, fraction):
if not self.color:
return ""
if fraction < 0.33:
return "\033[91m"
elif fraction < 0.66:
return "\033[93m"
return "\033[92m"
def _format_time(self, seconds):
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
s = int(seconds % 60)
return f"{h:02d}:{m:02d}:{s:02d}"
def update(self, n=1):
if self.start_time is None:
self.start_time = time.time()
self.current = min(self.current + n, self.total)
fraction = self.current / self.total
filled = int(self.bar_length * fraction)
color = self._get_color(fraction)
reset = "\033[0m" if self.color else ""
bar = color + "█" * filled + reset + "░" * (self.bar_length - filled)
elapsed = time.time() - self.start_time
if self.current > 0:
rate = self.current / elapsed
remaining = (self.total - self.current) / rate
eta_str = self._format_time(remaining)
rate_str = f"{rate:.1f} it/s"
else:
eta_str = "--:--:--"
rate_str = "-- it/s"
percent = fraction * 100
line = f"\r{self.label}: [{bar}] {percent:5.1f}% | ETA: {eta_str} | {rate_str}"
sys.stdout.write(line + "\033[K")
sys.stdout.flush()
def finish(self):
self.update(0)
print()
# 使用例
bar = ProgressBar(total=100, label="Training")
for i in range(100):
time.sleep(0.03)
bar.update()
bar.finish()
出力例:
Training: [██████████████████████████████] 100.0% | ETA: 00:00:00 | 32.8 it/s
このクラスは約60行で、tqdm の基本機能をカバーしています。必要に応じてコンテキストマネージャ(__enter__ / __exit__)やイテラブルのラッパーを追加すれば、さらに便利に使えます。
tqdm 実戦パターン:tqdm.tqdm / tqdm.notebook / tqdm.auto
自作はターミナル制御の理解には最適ですが、本番コードでは tqdm を素直に使うほうが安全です。tqdm には実行環境ごとに最適な実装が複数用意されています。
# 標準: ターミナル向け
from tqdm import tqdm
for x in tqdm(range(1000), desc="train"):
...
# Jupyter Notebook 向け(HTML ベースのバー)
from tqdm.notebook import tqdm as tqdm_nb
for x in tqdm_nb(range(1000), desc="epoch"):
...
# 環境を自動判定(CLI/Notebook/IPython のどれでも最適化)
from tqdm.auto import tqdm
ライブラリやスクリプトを書く側であれば from tqdm.auto import tqdm を採用するのが安全です。Jupyter 実行時は tqdm.notebook 相当、ターミナル実行時は標準実装に自動で切り替わります。
bar_format でカスタム書式を作る
tqdm の bar_format 引数は Python の str.format スタイルで表示テンプレートを完全に制御できます。実験ログを構造化したいときに特に有用です。
from tqdm import tqdm
fmt = "{l_bar}{bar:30}{r_bar} | loss={postfix[0]:.4f}"
with tqdm(range(200), bar_format=fmt, postfix=[0.0]) as pbar:
for step in pbar:
loss = 1.0 / (step + 1)
pbar.postfix[0] = loss
pbar.update(0) # 表示だけ更新
主なプレースホルダ:
| プレースホルダ | 内容 |
|---|---|
{l_bar} | 説明 + パーセンテージ |
{bar:N} | 幅 N のバー本体 |
{r_bar} | カウント + 経過 + ETA + レート |
{n_fmt} / {total_fmt} | 現在値 / 合計(K, M フォーマット可) |
{rate_fmt} | 「12.5it/s」「800ms/it」など |
{elapsed} / {remaining} | 経過時間 / 残り時間 |
{postfix} | set_postfix(loss=0.12, acc=0.98) の内容 |
set_postfix を使うと、学習中の指標を tqdm の右側にリアルタイム表示できます。
for x in (pbar := tqdm(range(N))):
pbar.set_postfix(loss=f"{loss:.3f}", acc=f"{acc:.3f}")
nested bar:leave=False と position
巣状ループ(外側=エポック、内側=バッチ)は position と leave=False の組み合わせが定番です。
from tqdm import tqdm
for epoch in tqdm(range(EPOCHS), desc="epoch", position=0):
for batch in tqdm(range(N_BATCH), desc="batch",
position=1, leave=False):
...
leave=False を内側に指定すると、完了時にその行が消えるため画面が散らかりません。マルチプロセスの場合は tqdm.set_lock(RLock()) でロックを共有し、position を被らせないようにすると行衝突を防げます。
tqdm.contrib.concurrent.process_map:並列処理に直結
並列計算の進捗表示は本記事で扱う他の長時間計算(ベイズ最適化・遺伝的アルゴリズム・シミュレーテッドアニーリング・モンテカルロ)と組み合わせると効きます。tqdm.contrib.concurrent は ProcessPoolExecutor を包む高水準 API を提供します。
from tqdm.contrib.concurrent import process_map, thread_map
def heavy(seed):
# 重い計算(例:1試行のモンテカルロ)
...
return result
# CPU バウンド(プロセス並列)
results = process_map(heavy, range(10_000), max_workers=8, chunksize=20)
# I/O バウンド(スレッド並列)
results = thread_map(fetch, urls, max_workers=32)
chunksize を 1 にすると進捗の粒度は細かいですが overhead が増えます。1 万件規模なら chunksize=20〜100 が現実的なバランスです。
pandas 連携:df.progress_apply
tqdm.pandas() を一度呼ぶだけで、DataFrame.apply / groupby.apply / Series.map に progress_apply / progress_map メソッドが生えます。
import pandas as pd
from tqdm import tqdm
tqdm.pandas(desc="feature")
df["price_log"] = df["price"].progress_apply(lambda x: math.log1p(x))
df.groupby("user_id").progress_apply(extract_features)
データ加工パイプラインで「いつ終わるのか」が読めない状況を一行で解消できるので、データ前処理スクリプトには常に入れておくと便利です。
tqdm vs rich.progress vs alive-progress 比較
最近は rich 系の代替も普及しています。用途別の使い分けを表にまとめます。
| 観点 | tqdm | rich.progress | alive-progress |
|---|---|---|---|
| 依存追加サイズ | 軽量(純 Python、依存ほぼなし) | 中程度(rich 一式が必要) | 軽量 |
| マルチバー | position 手動管理 | Progress コンテキストでネイティブ対応 | スピナー併用が容易 |
| カラーリング | ANSI 直書きで自由 | スタイル DSL([bold red] 等)でリッチ | アニメーション豊富 |
| Jupyter 対応 | tqdm.notebook で HTML 描画 | rich.jupyter で対応 | 限定的(CLI 推奨) |
| pandas 連携 | tqdm.pandas() で簡単 | 公式連携なし(手動ラップ) | なし |
| 並列処理ヘルパ | tqdm.contrib.concurrent | Progress.add_task を手動更新 | なし |
| ログとの混在 | 注意が必要(行が乱れがち) | Console.log と統合可 | 補助 print あり |
| 用途 | ML/データ処理の標準 | TUI を作る・凝った CLI | 単一処理の楽しい可視化 |
実務での目安:
- 大量データの ETL や ML 学習 →
tqdm.auto+tqdm.pandas+process_map - 配信ツール・自作 CLI で見た目を整えたい →
rich.progress - 個人プロジェクトで遊び心が欲しい →
alive-progress
実測:プログレスバーのオーバーヘッドはどれくらいか
ここまでは機能比較でしたが、「プログレスバーを付けると実際どれだけ遅くなるのか」は数字で見たことがない人が多いはずです。ここでは time.perf_counter() を使い、100万回のトリビアルなループ((i * i) % 97 を足し込むだけ)に対して、プログレスバーなし/tqdm/rich.progress/alive-progress を実行したときの壁時計時間を実測します。
検証環境:Apple M1、Python 3.14.6、tqdm 4.69.0、rich 15.0.0、alive-progress 3.3.0。出力は本物のターミナルではなく、リダイレクト先のファイルに書き込んでいます(後述のエッジケース2で扱う「非TTY」相当の状況です)。各設定を5回実行し中央値を採用しています。ベンチマークは実行環境に強く依存するので、絶対値ではなく「相対的にどれくらい違うか」という傾向を見てください。
import time
N = 1_000_000
def trivial_step(i, total):
return total + (i * i) % 97
def baseline(n):
total = 0
for i in range(n):
total = trivial_step(i, total)
return total
t0 = time.perf_counter()
baseline(N)
t1 = time.perf_counter()
print(f"baseline: {t1 - t0:.3f}s") # -> baseline: 0.065s
tqdm はデフォルト設定(for i in tqdm(range(n)))、mininterval=0, miniters=1 を指定して「毎回同期的に再描画・flush する」ように強制した場合、そして手動で1000回に1回だけ pbar.update(1000) する場合の3パターンを比較しました。rich.progress はデフォルト(refresh_per_second=10)と、ほぼ毎イテレーション再描画に相当する refresh_per_second=1000 を比較しています。alive-progress はデフォルト設定(force_tty=True で非TTY環境でもアニメーションを強制有効化)です。比較対象として、本記事の前半で自作した DIY バー(sys.stdout.write + flush())も同条件で計測しました。
| 設定 | 100万イテレーションの時間 | baseline比 |
|---|---|---|
| baseline(プログレスバーなし) | 0.065 s | — |
| DIY バー:1000回に1回だけ write+flush | 0.083 s | +27.8% |
tqdm:手動スロットル(1000回に1回 update) | 0.098 s | +50.9% |
tqdm:デフォルト設定 | 0.135 s | +108.2% |
alive-progress:デフォルト | 0.713 s | +997.6% |
rich.progress:デフォルト(10Hz) | 0.729 s | +1022.0% |
rich.progress:refresh_per_second=1000 | 0.733 s | +1027.1% |
| DIY バー:毎回 write+flush | 2.157 s | +3218.7% |
tqdm:mininterval=0(毎回同期 flush) | 18.99 s | +29116.0% |

いくつか意外な発見があります。
tqdmのデフォルトはかなり軽い(+108%、絶対値では約0.07マイクロ秒/回)。これはtqdmが内部で「前回の更新からmininterval(デフォルト0.1秒)経っていなければ再描画をスキップする」仕組み(dynamic_miniters)を持っているためです。mininterval=0を指定した瞬間、tqdmは約140倍遅くなる(0.135秒 → 18.99秒)。これは「フォーラムのコピペでmininterval=0にすると表示が滑らかになる」という誤解に基づくアンチパターンの実測結果です。rich.progressとalive-progressはrefresh_per_secondを 10 から 1000 に変えてもほぼ変化しない(0.729秒 → 0.733秒)。これは重要な設計上の違いで、次の節で説明します。- 手動スロットル(1000回に1回だけ更新)はほぼ baseline まで性能を回復させる(+50.9%、絶対値では0.033マイクロ秒/回)。
なぜ更新が多すぎると遅くなるのか:I/O・flush のコストとアーキテクチャの違い
tqdm の mininterval=0 が140倍もの減速を招いた理由は、1回の update() 呼び出しのたびに次のすべてを同期的に実行しているためです。
- 経過時間・レート・ETA の再計算(
format_meter内の文字列フォーマット処理) - バー文字列の再構築(
{bar},{postfix}などのテンプレート展開) sys.stdout.write()の呼び出し(システムコール)sys.stdout.flush()の呼び出し(バッファをOSに強制的に引き渡す)
このうち特にコストが高いのが flush() です。通常、print や write はバッファリングされ、まとめてOSに渡されますが、flush() はその都度カーネルに書き込みを依頼するため、システムコールのオーバーヘッド(コンテキストスイッチ含む)を100万回分そのまま支払うことになります。実際、本記事の前半で自作した「毎回 flush() する」DIYバーも、フォーマット処理がほぼ皆無であるにもかかわらず baseline の約33倍(2.157秒)まで遅くなりました。tqdm はさらに文字列整形のコストが乗るため約292倍まで悪化します。
一方で rich.progress と alive-progress が refresh_per_second を変えてもほとんど遅くならなかったのは、「進捗を進める」ことと「画面を再描画する」ことをアーキテクチャ上で分離しているからです。Progress.update() は単に内部カウンタをインクリメントするだけの軽い処理で、実際の再描画はバックグラウンドスレッドが refresh_per_second の周期で担当します。そのため update() を100万回呼んでも、システムコールを伴う実際の描画は最大で「経過秒数 × refresh_per_second」回程度に抑えられます。tqdm はこれとは対照的に、update() 呼び出し自体の中で同期的に「描画するかどうか」を判定し、条件を満たせばその場で write+flush まで行う設計です。したがって tqdm では 呼び出し側が明示的にスロットルする必要がある一方、rich/alive-progress は設計によって自動的にある程度守られています(ただし呼び出し回数そのものに伴う純粋な関数呼び出しコストは残るため、baseline比 +998〜1027% という下駄は履いたままです)。
スロットリングで回復する量を実測する。 tqdm の更新バッチサイズ(何イテレーションに1回 update() を呼ぶか)を 1 から 50,000 まで変化させ、100万イテレーションの総時間を計測しました。
from tqdm import tqdm
def run(n, batch):
total = 0
with tqdm(total=n, mininterval=0, miniters=1) as pbar:
count = 0
for i in range(n):
total += (i * i) % 97
count += 1
if count >= batch:
pbar.update(count)
count = 0
if count:
pbar.update(count)
return total

batch=1(毎回更新)では18.6秒かかっていたのが、batch=100(100回に1回更新、それでも1万回描画される)で0.24秒、batch=1000 で0.08秒まで下がり、batch=5000 以降は baseline(0.065秒)とほぼ区別がつかなくなります。実務では tqdm(iterable, mininterval=0.1)(デフォルト)で十分なケースがほとんどですが、それでも視覚的な滑らかさと性能を両立させたい場合は、miniters を明示的に大きめの値(例えば total // 100)に設定するか、mininterval を0.1〜0.5秒程度に緩めるのが安全です。
エッジケース1:マルチプロセスでのプログレスバー衝突
tqdm.contrib.concurrent.process_map のような高水準APIを使わず、複数プロセスがそれぞれ独自に tqdm インスタンスを持つ場合、何も考えずに実装すると表示が壊れます。原因は単純で、position を指定しない場合すべてのバーがデフォルトの position=0(画面の同じ行)を取り合うためです。
import multiprocessing as mp
import time
from tqdm import tqdm
def worker_naive(worker_id, n=40):
# バグ:全ワーカーが position=0(デフォルト)のまま => 同じ行を奪い合う
for i in tqdm(range(n), desc=f"worker-{worker_id}", position=0, mininterval=0, miniters=1):
time.sleep(0.005)
if __name__ == "__main__":
procs = [mp.Process(target=worker_naive, args=(i,)) for i in range(4)]
for p in procs:
p.start()
for p in procs:
p.join()
これを実行し、生の出力バイト列を確認すると、4つのワーカーが \r を使って同じ行の先頭に戻りながら競合していることが分かります(実測、抜粋)。
\rworker-0: 0%| | 0/40 [00:00<?, ?it/s]\rworker-2: 0%| | 0/40 [00:00<?, ?it/s]\rworker-1: 0%| | 0/40 [00:00<?, ?it/s]\rworker-3: 0%| | 0/40 [00:00<?, ?it/s]\rworker-0: 2%|...
すべての書き込みが \r から始まり、カーソル移動(\033[nA のような行送り)が一切ないため、実ターミナルで見ると4本のバーが同じ行の上で常に上書きし合い、最終的に「どれか1つが一瞬見えては消える」という壊れた表示になります。
標準的な修正は、ワーカーごとに異なる position を割り当て、tqdm.set_lock() でロックを共有することです。
import multiprocessing as mp
import time
from tqdm import tqdm
def worker_fixed(worker_id, lock, n=40):
# 修正:position=worker_id で自分専用の行を確保し、
# 親プロセスから渡されたロックを共有する
tqdm.set_lock(lock)
for i in tqdm(
range(n), desc=f"worker-{worker_id}", position=worker_id,
mininterval=0, miniters=1,
):
time.sleep(0.005)
if __name__ == "__main__":
lock = mp.RLock()
procs = [mp.Process(target=worker_fixed, args=(i, lock)) for i in range(4)]
for p in procs:
p.start()
for p in procs:
p.join()
修正後の生バイト列を見ると、各ワーカーの更新の前後に \x1b[A(カーソルを1行上へ)というANSIシーケンスと \n が挿入され、自分の行だけを狙って移動してから書き込んでいることが分かります(抜粋)。
\rworker-0: 0%| | 0/40 [00:00<?, ?it/s]\n\rworker-1: 0%| | 0/40 [00:00<?, ?it/s]\x1b[A\n\n\n\rworker-3: ...
tqdm.contrib.concurrent.process_map(本記事前半で紹介済み)は内部でこの position 管理を自動で行ってくれるため、単純な並列処理であれば手動で position を割り振るよりもそちらを使うほうが安全です。手動で Pool/Process を使う設計にせざるを得ない場合のみ、この「position + set_lock」パターンを覚えておいてください。
エッジケース2:非TTY(リダイレクト・CIログ)への出力
python script.py > out.log のように標準出力をファイルにリダイレクトしたり、CIのログのように非対話的な環境で実行したりすると、3つのライブラリはデフォルトでまったく異なる挙動を取ります。実際に30イテレーションのループを標準出力ではなくプレーンファイルへリダイレクトし、書き込まれたバイト列を調べました。
| ライブラリ | 挙動(デフォルト設定) | 書き込みバイト数 | \r の数 |
|---|---|---|---|
tqdm(オプション指定なし) | 律儀に \r で更新し続ける(スパムする) | 285 bytes | 5 |
rich.progress(オプション指定なし) | 非ターミナルを検知し、最終状態のみ1回描画 | 140 bytes | 0 |
alive-progress(オプション指定なし) | 非TTYを検知しアニメーションを全て無効化、最終レシートのみ | 155 bytes | 0 |
tqdm は disable を明示的に指定しない限り、TTYかどうかに関わらず \r ベースの更新を律儀に送り続けます。ファイルに書き出した後で cat すると、複数の進捗状態が \r で連結された読みにくい行になって現れます。一方 rich.progress と alive-progress は、出力先が端末でないことを自動検出し(Console.is_terminal / force_tty の内部判定)、途中経過を省略して最終状態だけを出力するという保守的なデフォルトを持っています。
明示的な検出と制御をしたい場合は、次のように書きます。
import sys
from tqdm import tqdm
# tqdm は自動判定してくれないので、自分で isatty() を見て明示的に disable する
is_interactive = sys.stdout.isatty()
for x in tqdm(range(1000), disable=not is_interactive, mininterval=0.5):
...
# あるいは disable=None にすると「ファイルにリダイレクトされたら自動的に無効化」される
for x in tqdm(range(1000), disable=None):
...
disable=None を指定すると、tqdm は file.isatty() を見て非TTY時に自動的にバー表示を止めてくれます(ただし disable のデフォルト値自体は False なので、何も指定しなければ常に表示されます)。CI環境のログを汚したくない場合は、disable=None を使うか、sys.stdout.isatty() を自分でチェックして渡すのが確実です。rich/alive-progress は既定でこの判断を代わりにやってくれますが、逆に「リダイレクト先でも必ずアニメーションさせたい」場合は force_terminal=True(rich)や force_tty=True(alive-progress)で明示的に上書きできます。
最近のアップデート(2024–2025)
tqdm:2024年に CLI 引数の型変換にeval()を使っていた脆弱性(CVE-2024-34062、CLIの--delim等の非boolean引数経由でコード実行が可能)が報告され、v4.66.3 でeval()を廃した安全な型変換に修正されました。tqdmをCLIとして(python -m tqdm経由で)使っている場合は、4.66.3以降を使ってください。通常のfrom tqdm import tqdmとしてのライブラリ利用には影響しません。alive-progress:2024年10月に v3.1.5、2025年7月に v3.2.0 がリリースされるなど、開発は継続的に行われています。tqdm.rich(tqdm同梱の rich 連携モジュール)は実験的な位置づけのまま、rich 本体のAPI変化に追随しきれていない部分があるため、両者を組み合わせたい場合はtqdm.richを経由するより本記事のようにrich.progressを直接使うほうが安定します。
いずれのライブラリも大枠のAPI(tqdm(iterable), Progress(), alive_bar(total))は安定しており、本記事のコード例は今後も通用するはずですが、CLIツールとしてtqdmを叩くケースがある場合はバージョンだけ確認しておくと安全です。
関連記事
- ベイズ最適化(Bayesian Optimization)の理論とPython実装
- 獲得関数の評価を多数回繰り返すため
tqdmのbar_formatで目的関数値を表示すると挙動が追いやすくなります。 - 遺伝的アルゴリズム(GA)の理論とPython実装
- 世代ごとのループに
tqdmの nested bar を仕込むと収束過程が一目で分かります。 - シミュレーテッドアニーリング(SA)の理論とPython実装
- 受理率・温度の可視化に
set_postfixを使う実用例として相性が良いです。 - モンテカルロ法(CEM)
- 大量サンプリングを並列化する際に
tqdm.contrib.concurrent.process_mapが直接活きます。 - Pythonデコレータ完全ガイド - プログレスバーをデコレータとして実装するパターンに応用できます。
- Python asyncio入門 - 非同期処理の進捗表示にプログレスバーを組み合わせる方法があります。
- Python正規表現パターン集 - Pythonの実践的なTipsを紹介しています。
- Matplotlib実践Tips:論文品質のグラフを作る - データ処理結果の可視化に役立つTipsを紹介しています。
- ガウス過程回帰の基礎と Python 実装
— 対数周辺尤度最適化や獲得関数評価で多数の反復を回す GP / ベイズ最適化に
tqdmのset_postfixで目的値表示が直結します。 - サポートベクターマシン(SVM)
—
GridSearchCVで C / gamma を多数試行する際にtqdmでフォールドごとの進捗を可視化すると待ち時間が読めるようになります。