Help us understand the problem. What is going on with this article?

Pythonで進捗表示したい!

概要

時間のかかる処理を実行した時、応答がないと「今どれくらい処理が進んでいるのか」「というか動いているのか」などなど、不安になることってありませんか?ありますよね?そう、あるんですよ(3段活用)。
ということでループ処理の進捗状況を表示する方法を覚書しておきます。

↓↓↓ちなみにこんな感じです↓↓↓
progress_DB_correct.gif
何かの役に立ちましたらぜひLGTM・ストック・コメントいただけると嬉しいです。

Vimmerはよければこちらもどうぞ見てってください↓
VimでPython書きたい人へ

目次

パッケージtqdmの利用

進捗表示の王道(だと勝手に思ってます)、tqdmをまずは紹介しておきます。
使い方は非常に簡単、importしてループに次のように組み込むだけです。

tqdm_test.py
%!pip install tqdm
import tqdm
import numpy as np


for i in tqdm.tqdm(range(int(1e7))):
    np.pi*np.pi

これで次のように表示されます。
tqdm_test.gif
便利ですね〜
tqdm.tqdm()に渡すのはイテラブル(ループ処理可能)なオブジェクトなので、他にもリストやらディクショナリやら文字列やらを渡すことができます。
ただし、注意点としてループ処理の中に標準出力であるprint関数などがあるとひどいことになります。

tqdm_test.py
%!pip install tqdm
import tqdm
import numpy as np


for i in tqdm.tqdm(range(int(1e7))):
    np.pi*np.pi
    if i % 1e6 == 0:
        print(i)

tqdm_test_fail.png
同じ理屈で、ループのネスト(入れ子)に対してtqdm.tqdm関数で進捗表示するとひどいことになります。

tqdm_test.py
import tqdm
for t in tqdm.tqdm(range(10)):
    for i in tqdm.tqdm(range(int(1e6))):
        np.pi*np.pi

tqdm_test_nest.gif
こういうの、ごくまれに不便なんですよね...皆さんもそんな経験ありませんか?ありますよね?そう、あるんですよ(3段活用)。
ということで自分でなんとかしてみましょう。

tqdmでネストループの進捗表示

tqdmでもネストループできることが判明しました。

tqdm_test.py
import time
from tqdm.notebook import trange


for t in trange(10):
    for i in trange(10, leave=False):
        np.pi*np.pi
        time.sleep(1/2**4)

tqdm_nest.gif
jupyter notebookではtqdm.notebooktrangeと言う関数を用いるとjupyter notebookでもネストループの進捗を表示することができます。

ターミナルなどのコンソールでやる場合は

tqdm_test.py
import time


import numpy as np
import tqdm


for i in tqdm.tqdm(range(10)):
    for j in tqdm.tqdm(range(10), leave=False):
        np.pi*np.pi
        time.sleep(1/2**4)

のように、leaveオプションフラグを折ることで良い感じに出力してくれます。
tqdm_nest_console.gif

自分で作ってみる

少なくともPythonにおいて、標準出力された文字列がプログラムの制御から離れるのは改行された時点なのだそうです。
逆に言えば改行さえしなければ標準出力後もプログラムで操作可能ということです。

progress.py
for i in range(int(1e7)):
    if i % 1e4 == 0:
        print("\r", i, end="")
    np.pi*np.pi

test_print_r.gif
この\rend=""によって標準出力後もプログラムがその制御権を握り続けることができるようになります。
end=""は、print関数で出力される文字列の最後に追加する文字列を指定するためのオプションで、デフォルトでは\n(改行)が指定されています。そのため、end=""とすることで空文字を追加する(という言い方は変ですが)ように変更すると改行されず、したがってプログラムで標準出力を弄ることが可能となるわけです。
ここでも登場しましたが、バックスラッシュから始まる\r\nエスケープシーケンスといい、文字列の中に含めたい特殊な制御を可能とする文字列群となっています。

エスケープシーケンス

エスケープシーケンスは前述の通り、文字列に特殊な制御を施したい時に使用される文字列群のことを言います。代表的なものは以下の表の通りです。

エスケープシーケンス 効果
\b バックスペース
\n 改行
\t タブ
\r 行の先頭に戻る

処理的にはバックスラッシュがエスケープシーケンス記述の始まりを表しており、その後に続く文字列に対応した制御文や文字コードに変換されています。
そのため、ダブルクオテーション"などの文字列そのものの制御文に利用されている文字列を出力させる時にも利用されます。

test_esc.py
print("\"")
print("\'")
print("\\")

一応この機能を用いてUnicodeで文字列を読み込ませることも可能ですが、まあそんなことをすることはないでしょう。

test_esc.py
# Hello world!
print("\x48\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x21")

プログレスバーを作ってみる

ではエスケープシーケンス\rを利用してプログレスバーを自作してみましょう。

progress.py
import time
for i in range(50):
    print("\r[{}]".format("#"*i), end="")
    time.sleep(0.1)

progress_first.gif
とりあえずプログレスバーと呼べるものはできましたね!ちなみに、なぜかはわかりませんがformat関数を利用するとエスケープシーケンスが機能しません。(20/11/17追記)動きました。多分コード書き間違いか何かでした。
このままだと味気ないのでもっといろいろ工夫してみましょう。

progress.py
import time
epoch = 50
for i in range(epoch):
    bar = "="*i + (">" if i < epoch-1 else "=") + " "*(epoch-i-1)
    print("\r[{}] {}/{}".format(bar, i+1, epoch), end="")
    time.sleep(0.1)

解説としては、まずbarという変数にプログレスバーの本体文字列を書きます。
コードではプログレスバーの長さを固定するためにスペースで穴埋めをしたり、先頭は矢印にしたりしています。

他にも、tqdmではできなかったネストについても、自分で定義するだけですのでなんとでもできます。

progress.py
import time


t_epoch = 10
i_epoch = 50
lap_time = -1
start_time = time.time()
for t in range(t_epoch):
    t_per = int((t+1)/t_epoch*i_epoch)
    for i in range(i_epoch):
        i_per = int((i+1)/i_epoch*i_epoch)
        if i_per <= t_per:
            bar = ("progress:[" + "X"*i_per
                                + "\\"*(t_per-i_per)
                                + " "*(i_epoch-t_per) + "]")
        else:
            bar = ("progress:[" + "X"*t_per
                                + "/"*(i_per-t_per)
                                + " "*(i_epoch-i_per) + "]")
        time.sleep(1e-2)
        elapsed_time = time.time() - start_time
        print("\r{} {}s/{}s".format(
                bar,
                int(elapsed_time),
                int(lap_time*t_epoch) if lap_time > 0 
           else int(elapsed_time*(i_epoch/(i+1))*t_epoch)),
              end="")
    lap_time = (time.time() - start_time)/(t+1)

progress_second.gif
いきなり複雑になってしまいましたね...解説します。
t_peri_perというのはtのループやiのループでの進捗状況を文字列で表示するために必要な文字数を計算して保持しています。
barはプログレスバーそのものですが

  • tのループは\という文字列で進捗状況を表す
  • iのループは/という文字列で進捗状況を表す
  • 重なる部分はXという文字列で進捗状況を表す

というふうになるようにプログラムしています。バックスラッシュはエスケープシーケンスの開始文字なので"\\"とする必要がありますね。
lap_timeiのループ1周分にかかる時間を保持しています。平均を取ってより正確な値が出るようにしています。elapsed_timeは現在までの経過時間です。

progress.py
                int(lap_time*t_epoch) if lap_time > 0 
           else int(elapsed_time*(i_epoch/(i+1))*t_epoch))

の部分ですが、lap_timeが計算されている場合はそちらを、そうでない場合(つまりiのループに初めて入った時)は経過時間からラップタイムを推測して表示するようにしています。

これはあくまで一例ですので、皆さんもいろいろと考えてみてください。

発展:ANSIエスケープコード

ここまでの話は「改行があると標準出力を上書きできない」と言っていましたが、実は改行があっても操作可能だったりします。その方法がANSIエスケープコードです。

progress.py
import time
epoch = 25

print(" "*(epoch-10) + "彡ノノハミ ⌒ミ")
print(" "*(epoch-11) + " (´・ω・`)ω・`) 今だ!オラごと撃て!")
print(" "*(epoch-13) + "  ⊂∩   ∩つ )")
print(" "*(epoch-10) + "/   〈   〈")
print(" "*(epoch-11) + " ( /⌒`J ⌒し'")
print("\n\n")
print(" "*(epoch-1)  + "ノ")
print(" "*(epoch-4)  + "彡 ノ")
print(" "*(epoch-6)  + "ノ")
print(" "*(epoch-8)  + "ノノ   ミ ノノ")
print()
print(" "*(epoch-11) + "(´;ω;`)ω^`) クソがあああああああ!!!")
print(" "*(epoch-13) + "  ⊂∩   ∩つ )")
print(" "*(epoch-10) + "/   〈   〈")
print(" "*(epoch-11) + " ( /⌒`J ⌒し'")
print("\033[6A")
for i in range(epoch):
    bar = "弌"*i + "⊃" + " "*(epoch-i-1)
    print("\rにア" + bar + "]", "{}/{}".format(i+1, epoch), end="")
    time.sleep(0.1)
print()
print("\033[5B")

progress_DB.gif
個人情報の隠し方が雑なのはスルーしてください笑
皆さんご存知?のドラゴンボールの名シーン、ラディッツ戦のパロディAAです。(なんか見つけたので使いました)
AAの部分を表示するためのコードはまあ見ての通りです。ただ、予め全てのAAを出力してあることに注意です。
そして

progress.py
print("\033[6A")

によって予め開けておいた

progress.py
print(" "*(epoch-8)  + "ノノ   ミ ノノ")
print()
print(" "*(epoch-11) + "(´;ω;`)ω^`) クソがあああああああ!!!")

の空行部分に移動し魔貫光殺法をこれまでの要領で表示させています。
最後に

progress.py
print()
print("\033[5B")

で魔貫光殺法を改行し標準出力の最後の行まで移動しています。

さて、謎のコードが出てきていますが、これがANSIエスケープコードです。
Pythonでは次の表のANSIエスケープコードがあります。

ANSIエスケープコード 効果
\033[nA カーソルを上にn行移動
\033[nB カーソルを下にn行移動
\033[nC カーソルを右にn移動
\033[nD カーソルを左にn移動
\033[nE カーソルを下にn行移動した後、その行の先頭に移動
\033[nF カーソルを上にn行移動した後、その行の先頭に移動
\033[nG カーソルを左端から数えてn番目の位置に移動
\033[n;mH カーソルをコンソール上端から数えてn行目、左端から数えてmの位置に移動
\033[nJ n=0のときはカーソルより後ろの文字列(以降の行を含む)を全て削除、n=1のときはカーソルより前の文字列(以前の行を含む)を削除、n=2のときは全文字列(出力された全て)を削除
\033[nK n=0のときはカーソルより後ろの文字列を削除、n=1のときはカーソルより前の文字列を削除、n=2のときは行全体を削除Pythonでは動かないかもしれません。
\033[nS n行分コンソールを次にスクロール
\033[nT n行分コンソールを前にスクロール
\033[n;mf Hのときと同じくカーソル移動
\033[nm SGR: Select Graphic Renditionコマンド。グラッフィック制御を行う。詳細はこちら

なんのこっちゃ、という方は最初の4つくらいだけ使えるようにしておきましょう。
このANSIコードを利用することでさっきみたいな自由度の高い操作が可能となります。

ちなみにこのANSIコードは多分ターミナルなどのコンソールを利用した場合のみ使用できるっぽいので、jupyter notebookなどでは\rでの単行プログレスバーで我慢しましょう。

20/11/17追記

パワーアップしました。

progress.py
import time
epoch = 25

print("\n\n\n")
print(" "*(epoch-10) + "彡ノノハミ ⌒ミ")
print(" "*(epoch-11) + " (´・ω・`)ω・`) 今だ!オラごと撃て!")
print(" "*(epoch-11) + "⊂∩    ∩つ  )")
print(" "*(epoch-11) + " /   〈  〈")
print(" "*(epoch-11) + " (/⌒` J⌒し'")
print("\033[5A", end="")
flag = False
for i in range(epoch):
    bar = "\033[33m弌"*i + "⊃\033[37m"
    print("\r\033[32mにア" + bar, end="")
    if i == int(epoch*0.25):
        print("\033[E", end="")
        print(" "*(epoch-11) + " (´・ω・`)ω・`) 離せっ!!オ…オレが悪かった!!", end="")
        print("\033[A", end="")
    elif i == int(epoch*0.5):
        flag = True
        print("\033[E", end="")
        print(" "*(epoch-11) + " (´;ω;`)ω^`) ぐ…!!ち…ちくしょおおお!!!", end="")
        print("\033[A", end="")
    if flag:
        if i == int(epoch*0.5)+1:
            print("\033[F", end="")
            print(" "*(epoch-9)  + "ノ")
        elif i == int(epoch*0.5)+3:
            print("\033[2F", end="")
            print(" "*(epoch-8)  + "ノ")
            print(" "*(epoch-9)  + "彡 ノ")
        elif i == int(epoch*0.5)+5:
            print("\033[3F", end="")
            print(" "*(epoch-5)  + "ノ")
            print(" "*(epoch-8)  + "彡 ノ")
            print(" "*(epoch-9)  + "ノ")
        elif i == int(epoch*0.5)+7:
            print("\033[4F", end="")
            print(" "*(epoch-3)  + "ノ")
            print(" "*(epoch-6)  + "彡 ノ")
            print(" "*(epoch-8)  + "ノ")
            print(" "*(epoch-9)  + "ノノ   ミ ノノ")
    time.sleep(0.25)
print()
print("\033[5B", end="")

progress_DB_correct.gif

おわりに

ということで意外と複雑な標準出力のお話を覚書しておきました。まあプログレスバーを自作する必要のある人はそうそういなさそうですが...
まあこういう機能を利用すればいろいろ遊べる、ということでいいでしょうきっと。

参考

kuroitu
大学院生の備忘録です。細々とニッチな情報を集めて実装しながら勉強しています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away