I am trying to create a multipage dashboard with panel
.
My question is based on this SO answer:
import panel as pn
from panel.template import FastListTemplate
pn.extension()
class Page1:
def __init__(self):
self.content = pn.Column("# Page 1", "This is the content of page 1.")
def view(self):
return self.content
class Page2:
def __init__(self):
self.content = pn.Column("# Page 2", "This is the content of page 2.")
def view(self):
return self.content
def show_page(page_instance):
main_area.clear()
main_area.append(page_instance.view())
pages = {
"Page 1": Page1(),
"Page 2": Page2()
}
page_buttons = {}
for page in pages:
page_buttons[page] = pn.widgets.Button(name=page, button_type="primary")
# page_buttons[page].on_click(lambda event: show_page(pages[page]))
page1_button, page2_button = page_buttons.values()
page1_button.on_click(lambda event: show_page(pages["Page 1"]))
page2_button.on_click(lambda event: show_page(pages["Page 2"]))
sidebar = pn.Column(*page_buttons.values())
main_area = pn.Column(pages["Page 1"].view())
template = FastListTemplate(
title="Multi-Page App",
sidebar=[sidebar],
main=[main_area],
)
template.servable()
If I am uncommenting this line in the for
loop
page_buttons[page].on_click(lambda event: show_page(pages[page]))
and remove (or comment as below) the two on_click
statements
# page1_button.on_click(lambda event: show_page(pages["Page 1"]))
# page2_button.on_click(lambda event: show_page(pages["Page 2"]))
both links on the sidebar point to page 2.
Can somebody explain to me why this is the case and how I can fix this issue?
Note: Of course, for two pages, a for loop is not needed, however in my case, my app will include a few more pages, and I would like to make the code more robust (i.e. to avoid forgetting to add a page or a click event).
Thank you!
Note: page1_button, page2_button = page_buttons.values()
is currently only used because my for
loop does not work as intended right now.
You are running into one of the classical footguns in Python: late binding aka "capture by reference".
By the time your lambda callback is evaluated (on click), the page
variable points at page 2 (loop has ended), and both lambdas capture the variable, not its value at time of call.
The operative line should become:
page_buttons[page].on_click(lambda event, saved=page: show_page(pages[saved]))
This forces an early binding by storing the current value as a default paramater, while still in the loop context.
The canonical answer goes into more detail. Creating functions (or lambdas) in a loop (or comprehension).