升级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
相关产品推荐
相关产品推荐

