Hello,
I have a question which is a follow up of Inactive Tab computes when dynamic=True.
I want the code for rendering a tab only be executed when the tab is active, even if there are upstream changes to the parameters which would trigger the re-computation.
I have set dynamic=True when creating panel.Tabs and used a param.ParamMethod with lazy=True to wrap the content.
import panel as pn
import param
from time import sleep
from panel.widgets.base import WidgetBase
main_tabs = pn.Tabs(dynamic=True)
layout = pn.template.FastListTemplate()
layout.main.append(main_tabs)
class Tab1(WidgetBase):
increment_button = pn.widgets.Button(name="Increase test_index", button_type="primary")
@param.depends("increment_button.clicks", watch=False)
def _count_clicks(self):
return self.increment_button.clicks
def __panel__(self):
return pn.Row(
pn.Column("Content for Tab 1", self.increment_button),
pn.Column("Clicks:", self._count_clicks)
)
class Tab2(WidgetBase):
test_index = param.Integer(default=None, allow_refs=True)
def __init__(self, **params):
super().__init__(**params)
@param.depends("test_index", watch=False)
def _content(self):
sleep(1) # Simulating a long computation
return f"Content for Tab 2 (test_index: {self.test_index})"
def __panel__(self):
content = pn.param.ParamMethod(self._content, lazy=True)
return content
tab1 = Tab1()
tab2 = Tab2(test_index=tab1.increment_button.param.clicks)
main_tabs.append(("Tab 1", tab1))
main_tabs.append(("Tab 2", tab2))
layout.servable()
The method tab2._content() is not executed until I make tab2 active, even if test_index has changed, which is what I want.
But after tab2 has been made active once, even if I move back to tab1, the method tab2._content() is executed every time test_index is changed.
I understand that one way to achieve what I want is to check in tab2._content() if tab2 is currently active and skip the computation otherwise, plus add a dependency on main_tabs.active to make sure the content is rendered when the tab is selected
But I was wondering if there is an in-built way in panel, instead of re-implementing the logic myself.
I implemented the following solution, in case it might be useful for others.
- I have an
ActiveTabWatcher Parameterized class which exposes an active boolean parameter (true when a specific tab is active).
- I pass the
active parameter (as a reference) to the widget which needs to display the content only when the tab is active.
- I created a decorator
depends_when_active which can be used instead of param.depends on the function that needs to be computed only if the tab is active
Here is the full code:
import panel as pn
import param
from time import sleep
import functools
from panel.widgets.base import WidgetBase
class ActiveTabWatcher(param.Parameterized):
"""
Watches a specific tab in a `pn.Tabs` widget and exposes its active state
as a reactive `param.Boolean` parameter.
The `active` parameter is `True` when the watched tab is currently selected
and `False` otherwise. It updates automatically whenever the active tab changes.
"""
# This parameter will be True when the watched tab is active, False otherwise
active = param.Boolean(default=False)
def __init__(self, tabs: pn.Tabs, index_of_tab_to_watch: int, **params):
"""
Initialize the watcher and bind it to the given tab.
Parameters
----------
tabs : pn.Tabs
The `Tabs` widget to monitor.
index_of_tab_to_watch : int
Zero-based index of the tab whose active state should be tracked.
**params
Additional keyword arguments forwarded to `param.Parameterized`.
"""
# define a parameter that will be True when the watched tab is active, False otherwise
self.active = (tabs.active == index_of_tab_to_watch)
# define a callback to update the parameter when the active tab changes
def _update_tab_active_param(active):
self.active = (active == index_of_tab_to_watch)
# bind the callback to the Tabs active parameter
param.bind(_update_tab_active_param, tabs.param.active, watch=True)
super().__init__(**params)
def depends_when_active(*dependencies, watch: bool = False):
"""A custom version of `param.depends` that only triggers the decorated function when the "active" parameter is (or becomes) True.
This is useful for avoiding expensive computations when a certain tab or section of the UI is not active.
The "active" parameter is expected to be a `param.Boolean` that is present in the class where the decorated method is defined.
"""
def decorator(func):
@param.depends(*dependencies, "active", watch=watch)
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
# Get the current values of the dependencies
deps_vals = [getattr(self, dep) for dep in dependencies]
if not hasattr(wrapper, "dependencies_value"):
# First call, consider dependencies as changed
# N.B. This will also create the attribute "dependencies_changed" on the first call to the wrapper
wrapper.dependencies_changed = True
else:
# Check if any of the dependencies have changed only if they haven't already been marked as changed
if not wrapper.dependencies_changed:
wrapper.dependencies_changed = any(
deps_vals[i] != wrapper.dependencies_value[i] for i in range(len(dependencies))
)
# Store current dependencies values for next comparison
wrapper.dependencies_value = deps_vals
# Only call the original function if the "active" parameter is True and dependencies have changed
if self.active and wrapper.dependencies_changed:
wrapper.dependencies_changed = False
# store the last computed value in the wrapper function itself so that it can be returned when the new value of the function is not computed
wrapper.last_return_value = func(self, *args, **kwargs)
# Return the last computed value even if "active" is False or dependencies haven't changed
return getattr(wrapper, "last_return_value", None)
return wrapper
return decorator
main_tabs = pn.Tabs()
layout = pn.template.FastListTemplate()
layout.main.append(main_tabs)
class Tab1(WidgetBase):
increment_button = pn.widgets.Button(name="Increase test_index", button_type="primary")
@param.depends("increment_button.clicks", watch=False)
def _count_clicks(self):
return self.increment_button.clicks
def __panel__(self):
return pn.Row(
pn.Column("Content for Tab 1", self.increment_button),
pn.Column("Clicks:", self._count_clicks)
)
class Tab2(WidgetBase):
test_index = param.Integer(default=None, allow_refs=True)
active = param.Boolean(default=False, allow_refs=True) # This parameter will be True when Tab 2 is active, False otherwise
def __init__(self, **params):
super().__init__(**params)
@depends_when_active("test_index", watch=False)
def _content(self):
sleep(1) # Simulating a long computation
return f"Content for Tab 2 (test_index: {self.test_index})"
def __panel__(self):
return self._content
tab2_active = ActiveTabWatcher(main_tabs, index_of_tab_to_watch=1) # Tab 2 is at index 1
tab1 = Tab1()
tab2 = Tab2(active=tab2_active.param.active,
test_index=tab1.increment_button.param.clicks)
main_tabs.append(("Tab 1", tab1))
main_tabs.append(("Tab 2", tab2))
main_tabs.append(("Tab 3", "Content for Tab 3"))
layout.servable()
1 Like