Dashとは、Plotly社が作るOSSのウェブフレームワークです。これを使うと何がうれしいかというと、Plotlyというグラフ描画ツールが簡単に利用できるというメリットがあります。つまり、自由にダッシュボードが作成できます。
あと、Reactを使って作られているため、Pythonから簡単にReact体験ができるようなものとなっています。プログラミング初心者にいきなりReactは難しいが、Dashなら簡単に書けます。これを使ってちょっと動作を理解したら、Reactに移るなんてことも良いと思います。
2020年12月にPlotly/Dashの解説本を共著で出版しました。執筆時、Dashはver1.Xだったのですが、2021年8月にver2.Xに更新されました。
PyConJP2021でも3つDashを使ったトークがあったため、Dashを使われたい方が多いのかなぁなんて思いますが、一方で2.0対応の記事があまり見当たらない。そこで今回は、Dash2.0への更新のフォローアップを記事を書きます。
ver1.Xと2.Xの比較のために使うアプリケーションは、トップのgifにあるドロップダウンでグラフの種類を選べて、ボタンをクリックすると表示されるグラフの種類が変更されるというかんじの、簡単なものを使ってはじめます。
1.Xで書いたコードは次のようになります。30行くらい。
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import plotly.express as px
graph_types = [px.line, px.scatter, px.bar]
app = dash.Dash(__name__)
app.layout = html.Div([
dcc.Dropdown(
id='graph_selector',
options = [{'label': type_.__name__, 'value': num} for num, type_ in enumerate(graph_types)],
value=0
),
html.Button(id='my_button', children='Update Graph'),
dcc.Graph(id='my_graph')
])
@app.callback(Output('my_graph', 'figure'),
Input('my_button', 'n_clicks'),
state=State('graph_selector', 'value')
)
def update_graph(n_clicks, selected_value):
return graph_types[selected_value](x=[1,2,3,4,5], y=[2,1,3,2,5])
if __name__ == "__main__":
app.run_server(debug=True)
Dash自体をもうちょっと知ってからという方には、私の書いたQiitaの記事を参考にしていただけますと幸いです。
動作環境
動作環境は次の通りです。
Windows11 Pro
python3.10.1
dash 1.21.0 #dash ver1.Xのコード
dash 2.0.0 #dash ver2.Xのコード
pandas 1.3.5
公式を参考にしながら確認
Dashの公式サイトに2.0への移行情報があります。何事も公式を見るのが重要です。この記事で分からなかったら公式を参照してください。
そしてこの記事は、下のサイトを見ながらかいつまんで書いています。
Breaking changes
Breaking changesは次の2つです。
Callbacks
@app.callbackにstate=を使って渡してたら、2.0からはinputs=も指定してください(公式ではinputになっていますがinputsが正しいです)。
コードの変更
app.pyのコードはstate=のみ書いています。最初のコードはver1.Xではそのまま動作するのですが、ver2.Xで実行すると次のようなエラーが出ます。
ValueError: The state keyword argument may not be provided without the input keyword argument
というわけで、inputs=も付け加えましょう。
@app.callback(Output('my_graph', 'figure'),
inputs=Input('my_button', 'n_clicks'), #inputs=を付け加え
state=State('graph_selector', 'value')
)
これでver2.Xでも動作するようになりました。もしくは引数を指定しない方法もあります。こっちの方が個人的には好き。
@app.callback(Output('my_graph', 'figure'),
Input('my_button', 'n_clicks'),
State('graph_selector', 'value')
)
Python2 Support
もうPython2はサポート終わります。ですよねぇ。
更新の推奨
import Statements
app.pyをver2.Xで実行した際には次のような警告が表示されます。
UserWarning:
The dash_core_components package is deprecated. Please replace
`import dash_core_components as dcc` with `from dash import dcc`
import dash_core_components as dcc
C:\Users\hogaw\documents\test-env\dash2\app2.py:3: UserWarning:
The dash_html_components package is deprecated. Please replace
`import dash_html_components as html` with `from dash import html`
import dash_html_components as html
というわけで、importの部分とあと一か所を変更します
from dash import Dash, callback, html, dcc, Input, Output, State
(中略)
app = Dash(__name__)
シンプルにしたとありますが、シンプルになったかどうかは分かりません。しかしこうして実行すると、警告が出なくなります。
この警告は地味にうっとおしいので直した方が良いですね。
installs
パッケージのインストールもdashだけでいいから、dash_html_components, dash_core_components, dash_tableとかrequirements.txtに書いていたら更新しておいてください。
アプリを改善する新機能
Long Callbacks
Long Callbacksのドキュメントを参照しながら概略を確認します。かなり長いので詳細は各自ドキュメントをご参照ください。
長い時間かかるコールバックは、これまでタイムアウトしていたようです。これはほとんどのサーバがデフォルトで30秒くらいでタイムアウトするように設定されているからと書かれています。サーバの設定を変えてもいいけど、他に支障が出る可能性もある。そこでLong Callbacksが使えるとのこと。使い方は @app.callback => @app.long_callbackと書き直すだけのようです。
方法には2つの選択肢があり、DiskCacheを使う(プロダクション環境には推奨されない)か、[Celery](https://docs.celeryproject.org/en/stable/ プロダクション環境にはお勧めみたい)を使うみたいです。
ちなみに、私はこの二つについて全然わからないので、詳しい方おられたら解説コメントとか頂けると嬉しいです。ドキュメントを読んでいると、Celeryの方がjob queueを使うのでサーバのリソースを突然消費することがないとあります。
This is recommended for production as, unlike Disk Cache, it uses a job queue and won't accidentally consume all your server's resources.
サンプルではdiskcacheを使うので、必要なパッケージを追加します。
$ python -m pip install diskcache multiprocess psutil
Example1
コードはドキュメントを参照ください。time.sleepを使って2秒とめてみたいなことをやっています。コードを動作させると、cacheの下にdbが作られて「へぇー」という感じです。
Example2
こちらもコードは略。コールバックが走っている間、ボタンが押せないという事例です。
Example3
こちらも(略。コールバックのキャンセルを作れる事例。
Example4
コールバックのプログレスバーが出る事例
Example5
コールバックのプログレスバーのバーチャートによる表示。
まとめ
@app.long_callbackの引数などはgithubで確認しましょう。
Flexible Callback Signatures
Dash1.Xはコールバック関数の引数は順番が関係ありましたが、2.Xでは柔軟に運用できます。
ここからは次のように、インプットに3つの数値をもち、アウトプットも3つもつアプリケーションを例に、2.Xのコールバックの柔軟さを見ていきます。
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
app = dash.Dash()
app.layout = html.Div([
dcc.Input(id='in1', value='1'),
dcc.Input(id='in2',value='2'),
dcc.Input(id='in3', value='3'),
html.P(id='out1'),
html.P(id='out2'),
html.P(id='out3')
])
@app.callback(
Output('out1', 'children'),
Output('out2', 'children'),
Output('out3', 'children'),
Input('in1', 'value'),
Input('in2', 'value'),
Input('in3', 'value')
)
def update_calc(a, b, c):
if None in [a, b, c]:
dash.exceptions.PreventUpdate
int_a, int_b, int_c = int(a), int(b), int(c)
return int_a*int_a, int_b*int_b, int_c*int_c
app.run_server(debug=True)
1.Xのコールバックでは、コールバック関数の引数にInputで指定したコンポーネントの要素(idで指定)を順番に渡します。in1: a, in2: b, in3: cという順番でupdate_calc関数の引数を渡しています。
2.Xでは次のようにできます。引数inputsにdictでInputを渡します。そしてkeyを指定することにより、
@app.callback(
Output('out1', 'children'),
Output('out2', 'children'),
Output('out3', 'children'),
inputs=dict(
c = Input('in1', 'value'),
b = Input('in2', 'value'),
a = Input('in3', 'value'))
)
def update_calc(b, a, c):
if None in [a, b, c]:
dash.exceptions.PreventUpdate
int_a, int_b, int_c = int(a), int(b), int(c)
return int_a*int_a, int_b*int_b, int_c*int_c
上のようにinputsに利用する属性を辞書に渡すことにより、そのキーを引数に渡せるようになりました。コードを変更すると、表示される数値の順序が変わります。
Outputも同じ感じで変更できます。というわけで、元に戻します。
@app.callback(
output = dict(
o3 = Output('out1', 'children'),
o2 = Output('out2', 'children'),
o1 = Output('out3', 'children')),
inputs=dict(
c = Input('in1', 'value'),
b = Input('in2', 'value'),
a = Input('in3', 'value'))
)
def update_calc(b, a, c):
if None in [a, b, c]:
dash.exceptions.PreventUpdate
int_a, int_b, int_c = int(a), int(b), int(c)
return dict(o1 = int_a*int_a, o2 = int_b*int_b, o3=int_c*int_c)
今回はエラーが出た・・・。なぜでしょうか?分かりません。
詳細はドキュメントをご確認ください。
@dash.callback
@app.callbackの代わりに@dash.callbackが使えるということです。@app.clientside_callbackの代わりに@dash.clientside_callbackも使えるようです。
これは何がうれしいかと考えましたが、別ファイルにコールバックを分けて作る時にdash.Dashインスタンスをインポートしなくてもよくなるなぁと思いました。
Ver2.Xの場合 app.py
from dash import Dash, dcc, html
import callback
app = Dash(__name__)
app.layout = html.Div([
dcc.Input(id='my_input', value=1),
html.P(id='my_para')
])
app.run_server(debug=True)
callback.py
# 1.Xの場合、app.pyからappをインポートして@app.callback()という感じだった
from dash import callback, Input, Output
@callback(
Output('my_para', 'children'),
Input('my_input', 'value')
)
def update_para(a):
return a
まとめ
というかんじで、変更点を確認しました。
Dash2.0が出されたときに、一応本のコードが動くかは試して全部動いたので一安心でしたが、importの警告とかちょっと気になっていたのでなんか書くかと思った8月。気が付けば年末ということで、2021年に駆け込みできて良かったです。
あと、いろいろ新しいコンポーネント作成なパッケージもあるみたいですね。色々一応はみているのですが、2022年はその辺りも書ければいいなぁと思っております。皆さんよいお年を!!!