概要
時間のかかる処理を実行した時、応答がないと「今どれくらい処理が進んでいるのか」「というか動いているのか」などなど、不安になることってありませんか?ありますよね?そう、あるんですよ(3段活用)。
ということでループ処理の進捗状況を表示する方法を覚書しておきます。
↓↓↓ちなみにこんな感じです↓↓↓
何かの役に立ちましたらぜひLGTM・ストック・コメントいただけると嬉しいです。
目次
パッケージtqdm
の利用
進捗表示の王道(だと勝手に思ってます)、tqdm
をまずは紹介しておきます。
使い方は非常に簡単、importしてループに次のように組み込むだけです。
%!pipinstalltqdmimporttqdmimportnumpyasnpforiintqdm.tqdm(range(int(1e7))):np.pi*np.pi
これで次のように表示されます。
便利ですね〜tqdm.tqdm()
に渡すのはイテラブル(ループ処理可能)なオブジェクトなので、他にもリストやらディクショナリやら文字列やらを渡すことができます。
ただし、注意点としてループ処理の中に標準出力であるprint
関数などがあるとひどいことになります。
%!pipinstalltqdmimporttqdmimportnumpyasnpforiintqdm.tqdm(range(int(1e7))):np.pi*np.piifi%1e6==0:print(i)
同じ理屈で、ループのネスト(入れ子)に対してtqdm.tqdm
関数で進捗表示するとひどいことになります。
importtqdmfortintqdm.tqdm(range(10)):foriintqdm.tqdm(range(int(1e6))):np.pi*np.pi
こういうの、ごくまれに不便なんですよね...皆さんもそんな経験ありませんか?ありますよね?そう、あるんですよ(3段活用)。
ということで自分でなんとかしてみましょう。
自分で作ってみる
少なくともPythonにおいて、標準出力された文字列がプログラムの制御から離れるのは改行された時点なのだそうです。
逆に言えば改行さえしなければ標準出力後もプログラムで操作可能ということです。
foriinrange(int(1e7)):ifi%1e4==0:print("\r",i,end="")np.pi*np.pi
この\r
とend=""
によって標準出力後もプログラムがその制御権を握り続けることができるようになります。end=""
は、print
関数で出力される文字列の最後に追加する文字列を指定するためのオプションで、デフォルトでは\n
(改行)が指定されています。そのため、end=""
とすることで空文字を追加する(という言い方は変ですが)ように変更すると改行されず、したがってプログラムで標準出力を弄ることが可能となるわけです。
ここでも登場しましたが、バックスラッシュから始まる\r
や\n
はエスケープシーケンスといい、文字列の中に含めたい特殊な制御を可能とする文字列群となっています。
エスケープシーケンス
エスケープシーケンスは前述の通り、文字列に特殊な制御を施したい時に使用される文字列群のことを言います。代表的なものは以下の表の通りです。
エスケープシーケンス | 効果 |
---|---|
\b | バックスペース |
\n | 改行 |
\t | タブ |
\r | 行の先頭に戻る |
処理的にはバックスラッシュがエスケープシーケンス記述の始まりを表しており、その後に続く文字列に対応した制御文や文字コードに変換されています。
そのため、ダブルクオテーション"
などの文字列そのものの制御文に利用されている文字列を出力させる時にも利用されます。
print("\"")print("\'")print("\\")
一応この機能を用いてUnicodeで文字列を読み込ませることも可能ですが、まあそんなことをすることはないでしょう。
# Hello world!
print("\x48\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x21")
プログレスバーを作ってみる
ではエスケープシーケンス\r
を利用してプログレスバーを自作してみましょう。
importtimeforiinrange(50):print("\r["+"#"*i+"]",end="")time.sleep(0.1)
とりあえずプログレスバーと呼べるものはできましたね!ちなみに、なぜかはわかりませんがformat
関数を利用するとエスケープシーケンスが機能しません。
このままだと味気ないのでもっといろいろ工夫してみましょう。
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
ではできなかったネストについても、自分で定義するだけですのでなんとでもできます。
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)
いきなり複雑になってしまいましたね...解説します。t_per
やi_per
というのはt
のループやi
のループでの進捗状況を文字列で表示するために必要な文字数を計算して保持しています。bar
はプログレスバーそのものですが
t
のループは\
という文字列で進捗状況を表すi
のループは/
という文字列で進捗状況を表す- 重なる部分は
X
という文字列で進捗状況を表す
というふうになるようにプログラムしています。バックスラッシュはエスケープシーケンスの開始文字なので"\\"
とする必要がありますね。lap_time
はi
のループ1周分にかかる時間を保持しています。平均を取ってより正確な値が出るようにしています。elapsed_time
は現在までの経過時間です。
int(lap_time*t_epoch)iflap_time>0elseint(elapsed_time*(i_epoch/(i+1))*t_epoch))
の部分ですが、lap_time
が計算されている場合はそちらを、そうでない(つまりi
のループに初めて入った時)場合は経過時間からラップタイムを推測して表示するようにしています。
これはあくまで一例ですので、皆さんもいろいろと考えてみてください。
発展:ANSIエスケープコード
ここまでの話は「改行があると標準出力を上書きできない」と言っていましたが、実は改行があっても操作可能だったりします。その方法がANSIエスケープコードです。
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")
個人情報の隠し方が雑なのはスルーしてください笑
皆さんご存知?のドラゴンボールの名シーン、ラディッツ戦のパロディAAです。(なんか見つけたので使いました)
AAの部分を表示するためのコードはまあ見ての通りです。ただ、予め全てのAAを出力してあることに注意です。
そして
print("\033[6A")
によって予め開けておいた
print(" "*(epoch-8)+"ノノ ミ ノノ")print()print(" "*(epoch-11)+"(´;ω;`)ω^`) クソがあああああああ!!!")
の空行部分に移動し魔貫光殺法をこれまでの要領で表示させています。
最後に
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のときは行全体を削除 |
\033[nS | n行分コンソールを次にスクロール |
\033[nT | n行分コンソールを前にスクロール |
\033[n;mf | Hのときと同じくカーソル移動 |
\033[nm | SGR: Select Graphic Renditionコマンド。グラッフィック制御を行う。詳細はこちら |
なんのこっちゃ、という方は最初の4つくらいだけ使えるようにしておきましょう。
このANSIコードを利用することでさっきみたいな自由度の高い操作が可能となります。
ちなみにこのANSIコードは多分ターミナルなどのコンソールを利用した場合のみ使用できるっぽいので、jupyter notebookなどでは\r
での単行プログレスバーで我慢しましょう。
おわりに
ということで意外と複雑な標準出力のお話を覚書しておきました。まあプログレスバーを自作する必要のある人はそうそういなさそうですが...
まあこういう機能を利用すればいろいろ遊べる、ということでいいでしょうきっと。