Pythonで進捗表示したい！

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

↓↓↓ちなみにこんな感じです↓↓↓
![progress_DB_correct.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/63399dda-1817-af94-d542-49ba5fbfabd0.gif)
何かの役に立ちましたらぜひLGTM・ストック・コメントいただけると嬉しいです。

Vimmerはよければこちらもどうぞ見てってください↓
[VimでPython書きたい人へ](https://qiita.com/kuroitu/items/af04b9b3e60d2c649f53)

深層学習についても実装混じりでいろいろ書いてます(DNN〜CNNまで)↓
[深層学習入門 ~基礎編~](https://qiita.com/kuroitu/items/221e8c477ffdd0774b6b)


# 目次
- [パッケージ`tqdm`の利用](#パッケージtqdmの利用)
    - [`tqdm`でネストループの進捗表示](#tqdmでネストループの進捗表示)
- [自分で作ってみる](#自分で作ってみる)
    - [エスケープシーケンス](#エスケープシーケンス)
    - [プログレスバーを作ってみる](#プログレスバーを作ってみる)
- [発展：ANSIエスケープコード](#発展ansiエスケープコード)
- [20/11/17追記](#201117追記)


# パッケージ`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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/dfd87558-f791-25d8-6480-260409d412d6.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/ef6c883b-b082-5796-a9ff-ead341965f6b.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/80efc442-f322-a69d-7578-1931cdf67fe0.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/fcdc9dbc-875a-d491-beca-cd42d808cd6c.gif)
jupyter notebookでは`tqdm.notebook`の`trange`と言う関数を用いると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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/97663e8b-1004-5af9-143d-0c69059ac2b1.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/1f745b8b-2972-5363-65dc-7619c758f194.gif)
この`\r`と`end=""`によって標準出力後もプログラムがその制御権を握り続けることができるようになります。
`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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/3bfe3b8d-5c25-7d0d-37c7-ae8ed16edbce.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/994ef02d-aebf-ebea-3584-1c36bd4735d9.gif)
いきなり複雑になってしまいましたね...解説します。
`t_per`や`i_per`というのは`t`のループや`i`のループでの進捗状況を文字列で表示するために必要な文字数を計算して保持しています。
`bar`はプログレスバーそのものですが

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

というふうになるようにプログラムしています。バックスラッシュはエスケープシーケンスの開始文字なので`"\\"`とする必要がありますね。
`lap_time`は`i`のループ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) + " ( /⌒｀Ｊ ⌒し'")
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) + " ( /⌒｀Ｊ ⌒し'")
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/b0d43449-a5a8-ee2b-b904-4e6c9db9d46c.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コマンド。グラッフィック制御を行う。詳細は[こちら](https://www.mm2d.net/main/prog/c/console-02.html)|

なんのこっちゃ、という方は最初の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) + " (／⌒｀ Ｊ⌒し'")
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/63399dda-1817-af94-d542-49ba5fbfabd0.gif)


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


# 参考
- [エスケープシーケンス](https://www.mm2d.net/main/prog/c/console-01.html)
- [Unicode一覧表](http://www.shurey.com/js/works/unicode.html)
- [魔貫光殺砲のaaできたった](http://fanblogs.jp/matomeman/archive/1/0)
- [2chのかわいいAA/顔文字まとめ](https://2ch-aa.blogspot.com/2017/05/512.html)