Quantcast
Channel: ループタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 83

Pythonで進捗表示したい!

$
0
0

概要

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

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

目次

パッケージtqdmの利用

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

tqdm_test.py
%!pipinstalltqdmimporttqdmimportnumpyasnpforiintqdm.tqdm(range(int(1e7))):np.pi*np.pi

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

tqdm_test.py
%!pipinstalltqdmimporttqdmimportnumpyasnpforiintqdm.tqdm(range(int(1e7))):np.pi*np.piifi%1e6==0:print(i)

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

tqdm_test.py
importtqdmfortintqdm.tqdm(range(10)):foriintqdm.tqdm(range(int(1e6))):np.pi*np.pi

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

自分で作ってみる

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

progress.py
foriinrange(int(1e7)):ifi%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
importtimeforiinrange(50):print("\r["+"#"*i+"]",end="")time.sleep(0.1)

progress_first.gif
とりあえずプログレスバーと呼べるものはできましたね!ちなみに、なぜかはわかりませんがformat関数を利用するとエスケープシーケンスが機能しません。
このままだと味気ないのでもっといろいろ工夫してみましょう。

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

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

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

progress.py
importtimet_epoch=10i_epoch=50lap_time=-1start_time=time.time()fortinrange(t_epoch):t_per=int((t+1)/t_epoch*i_epoch)foriinrange(i_epoch):i_per=int((i+1)/i_epoch*i_epoch)ifi_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_timeprint("\r"+bar,"{}s/{}s".format(int(elapsed_time),int(lap_time*t_epoch)iflap_time>0elseint(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)iflap_time>0elseint(elapsed_time*(i_epoch/(i+1))*t_epoch))

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

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

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

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

progress.py
importtimeepoch=25print(" "*(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")foriinrange(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[nJn=0のときはカーソルより後ろの文字列(以降の行を含む)を全て削除、n=1のときはカーソルより前の文字列(以前の行を含む)を削除、n=2のときは全文字列(出力された全て)を削除
\033[nKn=0のときはカーソルより後ろの文字列を削除、n=1のときはカーソルより前の文字列を削除、n=2のときは行全体を削除
\033[nSn行分コンソールを次にスクロール
\033[nTn行分コンソールを前にスクロール
\033[n;mfHのときと同じくカーソル移動
\033[nmSGR: Select Graphic Renditionコマンド。グラッフィック制御を行う。詳細はこちら

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

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

おわりに

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

参考


Viewing all articles
Browse latest Browse all 83

Trending Articles