You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

升级Bokeh 3.1.1后切换布局内容触发DeserializationError问题

Bokeh版本升级后切换加载界面触发DeserializationError的问题及解决

问题背景

项目在Bokeh 2.4.3版本下运行正常,升级至3.1.1版本后,执行先显示加载界面再切换为实际图表的逻辑时,出现bokeh.core.serialization.DeserializationError错误。核心逻辑简化如下:

class MyFrame:

    def __init__(self):
        self.layout = column()
        self.loading = loading_pic()
        self.layout.children.append(self.loading)

    def display_loading(self) -> None:
        self.layout.children[0] = self.loading

    def plot(self, data) -> None:
        self.my_plot = MyPlot(data)
        self.layout.children[0] = self.my_plot 

错误信息

2023-06-19 13:29:44,116 error handling message
 message: Message 'PATCH-DOC' content: {'events': [{'kind': 'ModelChanged', 'model': {'id': 'p1004'}, 'attr': 'inner_width', 'new': 865}, {'kind': 'ModelChanged', 'model': {'id': 'p1004'}, 'attr': 'inner_height', 'new': 540}, {'kind': 'ModelChanged', 'model': {'id': 'p1004'}, 'attr': 'outer_width', 'new': 900}, {'kind': 'ModelChanged', 'model': {'id': 'p1004'}, 'attr': 'outer_height', 'new': 550}]}
 error: DeserializationError("can't resolve reference 'p1004'")

其中p1004是加载界面对象的ID。注释display_loading代码后错误消失,但加载功能失效;加载界面对象在报错时仍存在,初始加载也能正常显示,程序未崩溃但错误持续出现。

可复现代码

from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, Label, RangeSlider
from bokeh.layouts import column
from random import *

def loading_pic(width: int = 500, height: int = 500) -> figure:
    plot = figure(x_range=(0,width), y_range=(0,height), width=width, height=height, tools='')
    pos_x = width/2 
    pos_y = height/2    
    
    label_size = min(height/4, width/4)
    label_size_str = str(label_size)+'px'
    
    loading_label = Label(x=pos_x, y=pos_y, text='Loading', text_font_size = label_size_str,
                          text_align='center', text_baseline='middle')
    plot.add_layout(loading_label)
    
    plot.xaxis.visible = False
    plot.yaxis.visible = False
    plot.xgrid.grid_line_color = None
    plot.ygrid.grid_line_color = None
    
    return plot
    
def random_coords(maximum: float = 500, amount: int = 1000) -> ColumnDataSource:
    data = ColumnDataSource()
    data.data['x'] = [random()*maximum for i in range(amount)]
    data.data['y'] = [random()*maximum for i in range(amount)]
    return data
    
class MyPlot:
    
    def __init__(self, data: ColumnDataSource):
        self.plot = figure(x_range=(0,500), y_range=(0,500), width=500, height=500)
        self.plot.circle(source=data, x='x', y='y')
    
class MyFrame:

    def __init__(self):
        self.layout = column()
        self.loading = loading_pic()
        self.layout.children.append(self.loading)

    def display_loading(self) -> None:
        self.layout.children[0] = self.loading

    def plot(self, data) -> None:
        self.my_plot = MyPlot(data)
        self.layout.children[0] = self.my_plot.plot
        
    def plot_delayed(self, attr, old, new) -> None:
        self.display_loading()
        data = random_coords()
        curdoc().add_next_tick_callback(lambda: self.plot(data)) 
        
my_frame = MyFrame()
        
range_slider = RangeSlider(start = 0, end = 10, value = (0,10), step = 1, title = 'Range')
range_slider.on_change('value_throttled', my_frame.plot_delayed)
       
layout = column(my_frame.layout, range_slider)

curdoc().add_root(layout)

复现步骤:用Bokeh server运行代码,连续两次调整范围滑块即可触发错误。

原因分析

Bokeh 3.x版本对文档模型的引用管理做了更严格的优化:当加载界面被替换为图表后,原加载界面对象会被从文档的模型引用中移除(即使Python端仍持有引用),此时前端仍可能残留对该模型的尺寸更新事件。当再次将加载界面放回布局时,前端发送的旧尺寸更新事件会指向已被清理的模型ID,导致反序列化失败。

解决办法

方法1:每次显示加载界面时创建新实例

不再复用同一个加载界面对象,每次调用display_loading时重新创建:

def display_loading(self) -> None:
    self.loading = loading_pic()  # 重新创建加载界面
    self.layout.children[0] = self.loading

这样每次的加载界面都是全新的模型ID,不会和之前的残留事件冲突。

方法2:保持加载界面始终在文档中(隐藏/显示切换)

不替换布局中的子元素,而是通过设置visible属性控制加载界面和图表的显示状态:

class MyFrame:
    def __init__(self):
        self.layout = column()
        self.loading = loading_pic()
        self.my_plot = None
        self.layout.children.append(self.loading)

    def display_loading(self) -> None:
        if self.my_plot:
            self.my_plot.plot.visible = False
        self.loading.visible = True

    def plot(self, data) -> None:
        if not self.my_plot:
            self.my_plot = MyPlot(data)
            self.layout.children.append(self.my_plot.plot)
        else:
            # 更新图表数据,避免重复创建
            self.my_plot.plot.renderers[0].data_source.data = data.data
        self.loading.visible = False
        self.my_plot.plot.visible = True

这种方式加载界面始终存在于文档模型中,不会出现引用丢失的问题,同时也能实现加载状态切换。


内容的提问来源于stack exchange,提问作者Aziuth

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 15:27:07