やりたいこと
- dashでまあまあな規模のウェブページを作る
- app.pyに全部書くのは整備性がやばいので分割したい
例のごとく英語でもドキュメントが全然見つからないので併記します
splitting callback and components in multiple files
use the : deepl
ちなみに公式のサンプル自体は大量に存在するが、どいつもこいつもapp.pyが数百行あってやばい
dash触ってまだ2ヶ月くらいなので、よりよい解決法があれば教えて下さい
やったこと
ファイルの構造を考える
- 最低限コンポーネントとコールバックは切り出したい
- どんどん増えるであろうコンポーネントとコールバックをそれぞれ管理する奴も必要そう
早い話 ↓これを
.
├── app.py
├── assets
│ ├── common.css
│ └── default.css
└── src
└── some_utils.py
↓こんなかんじにすればよさそう
.
├── app.py
├── assets
│ ├── common.css
│ └── default.css
│
└── src
├── callback.py
├── callbacks
│ ├── hoge.py
│ ├── fuga.py
│ └── poyo.py
│
├── layout.py
├── components
│ ├── fizz.py
│ ├── buzz.py
│ ├── boo.py
│ └── bar.py
│
├── some_utils.py
└── utils
app.pyの整理
せっかくなので自分の別記事のソースを拝借する
Dashでdcc.CheckListのチェック状態を全選択・全解除するボタンの実装
python app.py
で起動し、http://localhost:8050 で閲覧できる
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_table
from dash.dependencies import Input, Output, State
from flask import Flask, request
import time
server = Flask(__name__)
app = dash.Dash(__name__, server=server)
app.title = 'checklist-test'
selected_key = None
checklists = dcc.Checklist(
id='checklist-states',
options=[
{'label': 'New York City', 'value': 'NYC'},
{'label': 'Montréal', 'value': 'MTL'},
{'label': 'San Francisco', 'value': 'SF'}
],
value=['MTL', 'SF']
)
app.layout = html.Div(id='main',children=[
html.H1(children='チェックリストのテスト'),
dcc.Location(id='location', refresh=False),
html.Div(className='main-block', children=[checklists]),
html.Div(className='second', children=[
html.Button('全選択', id='filter-check-button', className='filter_button'),
html.Button('全解除', id='filter-remove-button', className='filter_button')
])
])
@app.callback(
[Output('checklist-states', 'value')],
[Input('filter-check-button', 'n_clicks_timestamp'),
Input('filter-remove-button', 'n_clicks_timestamp')],
[State('checklist-states', 'value')]
)
def update_check(all_check, all_remove, checking):
if not all_check is None:
if (time.time() * 1000 - all_check) < 1000:
return [['NYC', 'MTL', 'SF']]
if not all_remove is None:
if (time.time() * 1000 - all_remove) < 1000:
return [[]]
if all_check is None and all_remove is None:
return [checking]
if __name__ == '__main__':
app.run_server(host='0.0.0.0', debug=True)
dashオブジェクトのapp
にレイアウトとコールバックが紐付いているので、別の関数にapp
を直接渡すことで処理してくれそう
というわけで未来の自分に仕事を投げつつapp.pyを削れるだけ削る
import dash
from flask import Flask
from src.layout import layout
from src.callback import callback
server = Flask(__name__)
app = dash.Dash(__name__, server=server)
app.title = 'checklist-test'
# componentsとcallback定義
app = layout(app)
callback(app)
if __name__ == '__main__':
app.run_server(host='0.0.0.0', debug=True)
tips:
コールバックの登録は絶対にコンポーネント定義の後である必要がある
importされた瞬間に入力値がnullのコールバックが走るため、Input/Output/Stateで見ているコンポーネントが存在しないとエラーになる
コンポーネントとレイアウト
言うまでもないが、ファイル名は適当に自分の納得したやつに変えてくださいね
レイアウトの切り出し
引数として受け取ったapp
の属性layoutに対して、html要素を代入する
import dash_core_components as dcc
import dash_html_components as html
def layout(app):
checklists = dcc.Checklist(
id='checklist-states',
options=[
{'label': 'New York City', 'value': 'NYC'},
{'label': 'Montréal', 'value': 'MTL'},
{'label': 'San Francisco', 'value': 'SF'}
],
value=['MTL', 'SF']
)
# app.layoutに要素を代入
app.layout = html.Div(id='main', children=[
html.H1(children='チェックリストのテスト'),
dcc.Location(id='location', refresh=False),
html.Div(className='main-block', children=[checklists]),
html.Div(className='second', children=[
html.Button('全選択', id='filter-check-button', className='filter_button'),
html.Button('全解除', id='filter-remove-button', className='filter_button')
])
])
return app
コールバックの切り出し
####tips:
直にupdate_check
を呼ぼうとするとデコレータ部分をうまく解釈できないので、一段階深くしてデコレータごと別の関数で囲う必要がある
(要は@app.callback()
のapp
が名前解決できる必要がある)
from dash.dependencies import Input, Output, State
import time
def callback(app):
@app.callback(
[
Output('checklist-states', 'value')
],
[
Input('filter-check-button', 'n_clicks_timestamp'),
Input('filter-remove-button', 'n_clicks_timestamp')
],
[
State('checklist-states', 'value')
]
)
def update_check(all_check, all_remove, checking):
if all_check is not None:
if (time.time() * 1000 - all_check) < 1000:
return [['NYC', 'MTL', 'SF']]
if all_remove is not None:
if (time.time() * 1000 - all_remove) < 1000:
return [[]]
if all_check is None and all_remove is None:
return [checking]
コンポーネントとコールバックが増えたときの対処
コンポーネントのさらなる分割
タイトル・チェックリスト・ボタンの3つを別々に管理する
layoutではブロックとスペーシングの管理のみ行う想定
tips:
html.Div()
に属性id
は必須ではない
from src.components import title, checklist, button
import dash_core_components as dcc
import dash_html_components as html
def layout(app):
app.layout = html.Div(id='main', children=[
html.Div(id='title-block', children=[title.layout()]),
dcc.Location(id='location', refresh=False),
html.Div(id='center-block', children=[
html.Div(children=checklist.layout()),
html.Div(children=button.layout())
])
])
return app
import dash_html_components as html
def layout():
return html.H1(id='title', children='チェックリストのテスト')
import dash_core_components as dcc
def layout():
return dcc.Checklist(
id='checklist-states',
options=[
{'label': 'New York City', 'value': 'NYC'},
{'label': 'Montréal', 'value': 'MTL'},
{'label': 'San Francisco', 'value': 'SF'}
],
value=['MTL', 'SF']
)
import dash_html_components as html
def layout():
return html.Button('全選択', id='filter-check-button', className='filter_button'), html.Button('全解除', id='filter-remove-button', className='filter_button')
コールバックのさらなる分割
同じように機能ごとに切り出す
callback.py
from src.callbacks import check_and_remove # hoge, fuga...
def callback(app):
check_and_remove.register(app)
# hoge.register(app)
# fuga.register(app)
check_and_remove.py
from dash.dependencies import Input, Output, State
import time
def register(app):
@app.callback(
[
Output('checklist-states', 'value')
],
[
Input('filter-check-button', 'n_clicks_timestamp'),
Input('filter-remove-button', 'n_clicks_timestamp')
],
[
State('checklist-states', 'value')
]
)
def update_check(all_check, all_remove, checking):
if all_check is not None:
if (time.time() * 1000 - all_check) < 1000:
return [['NYC', 'MTL', 'SF']]
if all_remove is not None:
if (time.time() * 1000 - all_remove) < 1000:
return [[]]
if all_check is None and all_remove is None:
return [checking]
最終的な構造
.
├── app.py
└── src
├── callback.py
├── callbacks
│ └── check_and_remove.py
├── components
│ ├── button.py
│ ├── checklist.py
│ └── title.py
└── layout.py