LoginSignup
7
2

More than 3 years have passed since last update.

plotly Dash でapp.pyからコンポーネントとコールバックを切り出す

Last updated at Posted at 2020-04-03

やりたいこと

  • 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 で閲覧できる

↓こんな画面になる
スクリーンショット 2020-02-19 15.43.06.png

app.py
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を削れるだけ削る

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要素を代入する

layout.py
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が名前解決できる必要がある)

callback.py
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は必須ではない

layout.py
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
title.py
import dash_html_components as html


def layout():

    return html.H1(id='title', children='チェックリストのテスト')
checklist.py
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']
    )
button.py
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
7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2