Jupyter Notebooks are a perfect fit for scientific work with Python. They combine the following elements:
This allows us to write scientifically meaningful, executable documents that contain results, their interpretation and their provenance. They are a key element for reproducible research.
Jupyter has a so-called rich display system. If Python code returns an object, Jupyter accesses special methods on the object to decide how to display it. This can involve pretty printing, HTML, images, video, sounds etc:
from PIL import Image
from io import BytesIO
import requests
response = requests.get(
"https://ssciwr.github.io/lunch-time-python/lunchtime5/thingstaette.png"
)
img = Image.open(BytesIO(response.content))
?img._repr_png_
img
ipywidgets
provides a number of widgets that are Python objects that display as HTML. The interactive behaviour of this HTML snippet is implemented in JavaScript and uses callback functions in Python. This way, you write interactive notebooks with pure Python.
import ipywidgets
button = ipywidgets.Button(description="Click Me!")
button
def handler(change):
button.description = "Thanks!"
button.on_click(handler)
We can create simple input fields that allow users to put in data. We can then access that data from Python reading and writing:
widget = ipywidgets.Text()
widget
widget.value
''
widget.value = "Test"
Many similar working subflavors exist (for a full list see the docs):
ipywidgets.FloatText(value=42.0, step=0.01)
ipywidgets.IntSlider(min=-10, max=10)
ipywidgets.Checkbox(value=True, description="Some Option")
widget = ipywidgets.Dropdown(options=["Model A", "Model B", "Model C"])
widget
widget.value
'Model A'
ipywidgets.RadioButtons(options=["Model A", "Model B", "Model C"])
ipywidgets.Select(options=["Linux", "Windows", "macOS"], description="OS:")
If multiple widgets should be placed together, possibly applying some styling, they can be grouped into container widgets. In contrast to other widgets, these do not have an accessible value
, but some have selected_index
:
widgets = [ipywidgets.Text(value=f"#{i}") for i in range(4)]
ipywidgets.HBox(children=widgets)
ipywidgets.VBox(children=widgets)
ipywidgets.Accordion(children=widgets, titles=tuple(f"Tab #{i}" for i in range(4)))
tab = ipywidgets.Tab(children=widgets, titles=tuple(f"Tab #{i}" for i in range(4)))
tab
tab.selected_index
0
import io
def img_to_widget(i):
membuf = io.BytesIO()
i.save(membuf, format="png")
return ipywidgets.Image(value=membuf.getvalue(), format="png")
img_widget = img_to_widget(img)
cropped_widget = img_to_widget(img)
x0 = ipywidgets.IntText(value=0, layout=ipywidgets.Layout(width="100px"))
y0 = ipywidgets.IntText(value=0, layout=ipywidgets.Layout(width="100px"))
x1 = ipywidgets.IntText(value=img.size[0], layout=ipywidgets.Layout(width="100px"))
y1 = ipywidgets.IntText(value=img.size[1], layout=ipywidgets.Layout(width="100px"))
controls = ipywidgets.VBox(
children=[
ipywidgets.VBox(children=[ipywidgets.Label("Upper left:"), x0, y0]),
ipywidgets.VBox(children=[ipywidgets.Label("Lower right:"), x1, y1]),
]
)
def crop_handler(_):
cropped_widget.value = img_to_widget(
img.crop([x0.value, y0.value, x1.value, y1.value])
).value
x0.observe(crop_handler, names="value")
y0.observe(crop_handler, names="value")
x1.observe(crop_handler, names="value")
y1.observe(crop_handler, names="value")
app = ipywidgets.AppLayout(
left_sidebar=controls,
center=img_widget,
right_sidebar=cropped_widget,
pane_widths=(1, 2, 2),
)
app
ipywidgets
contains a much simpler interface that automatically creates widgets for you. You simply need to annotate ("decorate") a function that does something and you will get a continuously updated interactive version:
@ipywidgets.interact(x=(0, 100), y=(0, 100))
def add(x, y):
return x + y
Notably, this does not change the function nature of add
. It is merely displaying a UI as a side effect of the function definition:
?add
ipywidgets.interact
has many more options and flavors. Here are some:
@ipywidgets.interact(
operation=[("add", 1.0), ("subtract", -1.0)],
rounding=False,
x=(0, 100, 0.1),
y=(0, 100, 0.1),
)
def op(operation, rounding, x, y):
val = x * operation + y
if rounding:
return round(val)
else:
return val
import time
@ipywidgets.interact_manual(x=(0, 100), y=(0, 100))
def slow_add(x, y):
time.sleep(1)
return x + y
For more information, see the ipywidgets
documentation: