IOとダックタイピング

# はじめに

今日、以下のようなプログラムを書いていました（一部わざとコメントを抜いています）

```py:app.py
from flask import Flask, render_template, send_file
from great_algorithm import GreatAlgorithm

app = Flask(__name__)
# 実際にはセッションごとにインスタンスを作ってるが割愛
algo = GreatAlgorithm()

@app.route('/', methods=['GET'])
def start():
    return render_template('index.html', algo=algo)

# パスパラメータ使って「何番目の画像」とかしてるわけだが割愛
@app.route('/image')
def image():
    return send_file(algo.get_image('jpg'), mimetype='image/jpeg')

@app.route('/', methods=['POST'])
def update():
    # フォーム値をupdateメソッドに渡すわけだが割愛
    algo.update()
    return render_template('index.html', algo=algo)

if __name__ == '__main__':
    app.run(debug=True)
```

```py:great_algorithm.py
import io
import numpy as np
import matplotlib.pyplot as plt

class GreatAlgorithm:
    def __init__(self):
        # 本題じゃないので適当
        img = np.random.random((400, 400))
        img = img * 255
        img = img.astype('uint8')
        self._img = img
    
    def update(self):
        # 本題じゃないので割愛
        pass
    
    def get_image(self, format):
        f = io.BytesIO()
        plt.imsave(f, self._img, format=format)
        f.seek(0)
        return f
```

# 何をしているのか

某エッチな絵を作るあれではないですが[^1]、「画像を提示」→「評価」→「評価に基づいて画像を更新」ということをしています。ただしその部分は今回の萌えポイントとは関係ないので割愛です。

[^1]: 一応私、「遺伝的アルゴリズムﾁｮｯﾄﾜｶﾙ」な人間なのですが、あらためて考えてみるとあれ提示される画像が2枚だけだけど、多様性考えると母集団が2個体だけなんてことはないよな、どう管理されてるんだろう（一定数保持して評価の低い古い世代から消していく？）と思いました。

元々Webインタフェースを作る気はなかったのですが、評価値を手入力するのがめんどくさいのでWeb化するかと思って「あれ？ここどうやればいいんだろう」と思って調べたところが今回の本題（萌えポイント）です。
画像は上記のようにNumPy配列です。ターミナル（と言っても別ウインドウは開きますが）でmatplotlib使ってimshowするだけならそれでまったく問題ありません。しかし、Web化するとなると「画像ファイル」を作る必要があります。

# 萌えポイント

## flask.send_file

先にflask側から。今までflaskで「動的に画像作って返す」ということをしたことがなかったので「flask return jpg」でググり以下の記事を見つけ、send_file使えばいけそうということはわかりました。
https://stackoverflow.com/questions/8637153/how-to-return-images-in-flask-response

ただ、「ファイルに書き出したくないな（一時ファイル作りたくないな）」という気持ちがあり[ドキュメント](https://flask.palletsprojects.com/en/1.1.x/api/#flask.send_file)を確認したところ、

> send_file(filename_or_fp, mimetype=None, as_attachment=False, attachment_filename=None, add_etags=True, cache_timeout=None, conditional=False, last_modified=None)
> 
> filename_or_fp – the filename of the file to send. This is relative to the root_path if a relative path is specified. Alternatively a file object might be provided in which case X-Sendfile might not work and fall back to the traditional method. Make sure that the file pointer is positioned at the start of data to send before calling send_file().

「ファイル名ではなく、ファイルオブジェクトを渡せる」ことが確認できました。

## matplotlib.pyplot.imsave

次に画像を作る側のmatplotlibです。[imsave](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.imsave.html)を使えば画像ファイルとして書き出せるわけですが、

> imsave(fname, arr, **kwargs)
fname : str or path-like or file-like

おぉ！こっちもファイルオブジェクトを渡せる！これで勝てる！

## io.BytesIO

「ファイルオブジェクト」と言っても、「本物のファイルを開いて（ファイルに対応するオブジェクトを作成して）」上記のsend_fileやimsaveに渡すわけではありません。
「ファイルのような挙動をする」オブジェクトを渡して`read`させたり`write`させたりすればいいわけです。そう、みんな大好きダックタイピングですね。readやwriteの呼び出しに応じてくれれば別に本物のファイルである必要はないわけです。

Pythonにはそのような「ファイルのような挙動をする」クラスがあらかじめ用意されています。[io.BytesIO](https://docs.python.org/ja/3/library/io.html#io.BytesIO)です。
というわけで一番の萌えポイントを今度はコメント付きで見てみましょう。

```py:great_algorithm.py抜粋
    def get_image(self, format):
        # ファイルに出力するのではなくメモリに出力させる
        f = io.BytesIO()
        plt.imsave(f, self._img, format=format)
        # 読み込み側が便利なようにoffsetを先頭にしておく
        f.seek(0)
        return f
```
```py:app.py抜粋
@app.route('/image')
def image():
    return send_file(algo.get_image('jpg'), mimetype='image/jpeg')
```

BytesIOオブジェクトに「画像ファイルの内容」を出力させ、それをそのままsend_fileに渡して最終的にブラウザまで届くようになっています。
一時ファイルなど作らないで済みます！
ちなみに、matplotlib側の`format`引数、flask側の`mimetype`引数は普通なら指定しませんが今回は「ファイル名から決定」ができないので指定する必要があります（指定しないとエラーになります）

# まとめ

- 保持している情報を画像ファイルにして返したいが一時ファイルは作りたくない
- flaskのsend_fileは「ファイルオブジェクト」を受け付けてくれる
- matplotlibのimsaveも「ファイルオブジェクト」を受け付けてくれる
- よしBytesIOの出番だ

「ファイルパスでもファイルオブジェクトでもよい」というのは使う側にとってはとてもありがたい仕様です。
作る側は大変ですが。特にPythonみたいな言語的にオーバーロードのない言語だと・・・

# オチ

その後に考えた結果、「世代ごとの画像」はログ的に残す必要あるなということで結局「ファイル出力」しないといけないかという考えに至りました。