在Dash中更灵活地编写回调函数

本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/dash-master

大家好我是费老师,使用Dash开发过交互式应用的朋友,想必都不会对回调函数感到陌生,作为Dash应用中实现各种交互逻辑的“万金油”方式,不管是常规的@app.callback(),还是对应浏览器端回调app.clientside_callback()ClientsideFunction(),其中编排各种回调角色时,我们都是按照先Output,再Input,最后State的顺序依次罗列的,且各个角色存在多个时,建议用[]将它们包裹住,以提升代码可读性。

但这并不是不可打破的铁律,事实上,Dash还额外提供了多种多样的回调角色编排方式,官方称之为Flexible Callback Signatures,从而解决单个回调函数中角色太多时代码可读性变差等问题,今天的文章中,我就将带大家学习相关的实用知识,从而更清晰地进行Dash应用开发及维护😇。

阅读本文大约需要6分钟

为了方便演示,我们构造下图所示的简单示例Dash应用(完整源码见文章开头地址):

如果要编排以两个按钮作为示例Input角色,两个输入框作为示例State角色,并向两个文字组件中分别Output不同的参数值内容的回调函数,按照常规的写法,对应的回调函数可以写作下方形式:

@app.callback(
    [Output('demo-output1', 'children'),
     Output('demo-output2', 'children')],
    [Input('demo-button1', 'nClicks'),
     Input('demo-button2', 'nClicks')],
    [State('demo-input1', 'value'),
     State('demo-input2', 'value')],
    prevent_initial_call=True
)
def demo_callback(nClicks1, nClicks2, value1, value2):

    return [
        f'nClicks1: {nClicks1}, nClicks2: {nClicks2}',
        f'value1: {value1}, value2: {value2}'
    ]

下面我们以此为基础,分别介绍其他不同的写法:

1 字典化角色编排

我们可以用字典来分别编排各类型的角色,其中具体可细分为:

  • InputState字典化

当仅对回调函数的InputState角色进行字典化编排时,我们可以通过自定义的键值对,完成针对回调函数输入参数的映射,改造后的示例回调函数如下:

@app.callback(
    [Output('demo-output1', 'children'),
     Output('demo-output2', 'children')],
    inputs=dict(
        nClicks1=Input('demo-button1', 'nClicks'),
        nClicks2=Input('demo-button2', 'nClicks')
    ),
    state=dict(
        value1=State('demo-input1', 'value'),
        value2=State('demo-input2', 'value')
    ),
    prevent_initial_call=True
)
def demo_callback(nClicks1, nClicks2, value1, value2):
    '''字典化角色编排:仅Input、State字典化'''

    return [
        f'nClicks1: {nClicks1}, nClicks2: {nClicks2}',
        f'value1: {value1}, value2: {value2}'
    ]
  • 全部角色字典化

如果我们将回调函数的Output也进行了字典化改造,那么在回调函数中就需要返回对应键值对的字典(返回单个dash.no_update时不受限制),示例写法如下:

@app.callback(
    output=dict(
        content1=Output('demo-output1', 'children'),
        content2=Output('demo-output2', 'children')
    ),
    inputs=dict(
        nClicks1=Input('demo-button1', 'nClicks'),
        nClicks2=Input('demo-button2', 'nClicks')
    ),
    state=dict(
        value1=State('demo-input1', 'value'),
        value2=State('demo-input2', 'value')
    ),
    prevent_initial_call=True
)
def demo_callback(nClicks1, nClicks2, value1, value2):
    '''字典化角色编排:全部角色字典化'''

    return dict(
        content1=f'nClicks1: {nClicks1}, nClicks2: {nClicks2}',
        content2=f'value1: {value1}, value2: {value2}'
    )

通过字典化角色的形式,我们可以为每个角色自由起名字,建议是起跟功能相关的名字,如login_button_click,或登录按钮点击这样的中文键名,只要能帮助你更好地读懂回调函数逻辑就可以😉。

2 嵌套式字典化角色编排

当我们在使用上文所介绍的字典化角色编排方式时,除了在字典中平铺书写相应角色外,还可以向下继续进行字典嵌套,从而实现更自由的参数分组效果,相应的,对应输入参数也会以字典的形式传入内部的各键值对参数:

@app.callback(
    output=dict(
        content1=Output('demo-output1', 'children'),
        content2=Output('demo-output2', 'children')
    ),
    inputs=dict(
        nClicks1=Input('demo-button1', 'nClicks'),
        nClicks2=Input('demo-button2', 'nClicks')
    ),
    state=dict(
        input_values=dict(
            value1=State('demo-input1', 'value'),
            value2=State('demo-input2', 'value')
        )
    ),
    prevent_initial_call=True
)
def demo_callback(nClicks1, nClicks2, input_values):
    '''嵌套式字典化角色编排'''

    return dict(
        content1=f'nClicks1: {nClicks1}, nClicks2: {nClicks2}',
        content2='value1: {value1}, value2: {value2}'.format(**input_values)
    )

3 对需要返回若干dash.no_update的情况进行简化

针对字典化角色编排Output的方式,当我们仅需要对部分输出目标返回实际值,对其余目标返回dash.no_update时,可以配合标准库collections中的defaultdict以及dash回调的上下文简化相关过程:

@app.callback(
    output=dict(
        content1=Output('demo-output1', 'children'),
        content2=Output('demo-output2', 'children')
    ),
    inputs=dict(
        nClicks1=Input('demo-button1', 'nClicks'),
        nClicks2=Input('demo-button2', 'nClicks')
    ),
    state=dict(
        value1=State('demo-input1', 'value'),
        value2=State('demo-input2', 'value')
    ),
    prevent_initial_call=True
)
def demo_callback(nClicks1, nClicks2, value1, value2):
    '''字典化Output配合defaultdict'''

    # 假设我们需要除了content1之外的其他角色默认输出为dash.no_update
    output = defaultdict(
        lambda: dash.no_update,
        dict(
            content1=f'nClicks1: {nClicks1}, nClicks2: {nClicks2}'
        )
    )

    return {
        key: output[key]
        # 通过上下文遍历所有Output字典键名
        for key in dash.ctx.outputs_grouping.keys()
    }

其中构造defaultdict并设置默认值等过程,我也会在fac即将发布的0.3.x版本中封装为一步到位的工具函数,毕竟这种场景在进阶Dash应用的开发中还是很常用的,省得在常规方式中逐个写dash.no_update或其他默认值。

除此之外,有关Flexible Callback Signatures还有一些其他的写法,但是在我看来并没有字典化写法这么实用,感兴趣的朋友可以移步https://dash.plotly.com/flexible-callback-signatures了解更多。


以上就是本文的全部内容,更多有关dash应用开发的前沿知识和技巧欢迎持续关注玩转dash公众号。

posted @ 2023-11-15 17:40  费弗里  阅读(766)  评论(0编辑  收藏  举报