在 Jupyter 中使用@interact 装饰器时实现一个 "reset" 按钮

Implement a "reset" button when using @interact decorator in Jupyter

我正在尝试做一个简单的按钮来将小部件“重置”为某些默认值。我在 Jupyter Lab 环境中使用 @interact 装饰器。问题是小部件标识符将它们的值复制到函数内用作浮点变量的相同标识符,因此我不能再在这个新范围内访问它们。这是一个简短的例子(不起作用):

import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, Button

@interact(starts_at=(0, np.pi*0.9, np.pi*0.1), ends_at=(np.pi, 2*np.pi, np.pi*0.1))
def plot_graph(starts_at=0, ends_at=2*np.pi):
    
    def on_button_clicked(_):
        # instructions when clicking the button (this cannot work)
        starts_at = 0
        ends_at = 2*np.pi
        
    button = Button(description="Reset")
    button.on_click(on_button_clicked)
    display(button)
    
    f = lambda x : sum(1/a*np.sin(a*x + np.pi/a) for a in range(1,6))
    x = np.linspace(0, 2*np.pi, 1000)
    plt.plot(x, f(x))
    plt.xlim([starts_at, ends_at])

有人知道如何将对原始小部件对象的引用发送到装饰函数的范围吗?我也将接受实现重置这些滑块的按钮的简单方法。

:-D

编辑:更正文本流

我可以在 不使用 的情况下使用 @interact 装饰器(现在它正在工作),但我对最终结果并不满意。所以,我仍然愿意为可以制作 clear/easier pythonic 版本的人提供正确答案状态

无论如何,这是工作代码:

import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, Button, FloatSlider

def plot_graph(starts_at, ends_at):
    f = lambda x : sum(1/a*np.sin(a*x + np.pi/a) for a in range(1,6))
    x = np.linspace(0, 2*np.pi, 1000)
    plt.plot(x, f(x))
    plt.xlim([starts_at, ends_at])
    
starts_at = FloatSlider(min=0, max=np.pi*0.9, value=0, step=np.pi*0.1)
ends_at = FloatSlider(min=np.pi, max=2*np.pi, value=2*np.pi, step=np.pi*0.1)

def on_button_clicked(_):
    starts_at.value = 0
    ends_at.value = 2*np.pi

button = Button(description="Reset")
button.on_click(on_button_clicked)
display(button)
_ = interact(plot_graph, starts_at=starts_at, ends_at=ends_at)

编辑:从这一点开始的新方法 ============================ ==

我选择@Ianhi 的答案是正确的,因为他指出了在我遇到问题时需要考虑的问题。谢谢!

无论如何,我在这里发布了我正在使用的最终方案,它足够简单,可以满足我的需要,我可以在所有交互中重复使用我的重置按钮:

# Preamble ----
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, Button, FloatSlider

def reset_button(defaults={}):
    def on_button_clicked(_):
        for k, v in defaults.items(): 
            k.value = v
    button = Button(description='Reset')
    button.on_click(on_button_clicked)
    display(button)

# Code ----
slider1 = FloatSlider(min=0, max=np.pi*0.9, value=0, step=np.pi*0.1)
slider2 = FloatSlider(min=np.pi, max=2*np.pi, value=2*np.pi, step=np.pi*0.1)
reset_button(defaults={slider1: 0, slider2: 2*np.pi})

@interact(starts_at=slider1, ends_at=slider2)
def plot_graph(starts_at, ends_at):
    f = lambda x : sum(1/a*np.sin(a*x + np.pi/a) for a in range(1,6))
    x = np.linspace(0, 2*np.pi, 1000)
    plt.plot(x, f(x))
    plt.xlim([starts_at, ends_at])

要完成此操作,您必须使用更手动的 interactive_output 功能。该函数允许您 pre-create 小部件,然后将它们传入:

import ipywidgets as widgets
import numpy as np
import matplotlib.pyplot as plt

start_slider = widgets.FloatSlider(
                            val = 0,
                            min = 0,
                            max = np.pi*0.9,
                            step = np.pi*0.1,
                            description = 'Starts at'
                            )
end_slider = widgets.FloatSlider(
                            val = np.pi,
                            min = np.pi,
                            max = 2*np.pi,
                            step = np.pi*0.1,
                            description = 'Ends at'
                            )
def on_button_clicked(_):
    start_slider.value = 0
    end_slider.value = 2*np.pi

button = Button(description="Reset")
button.on_click(on_button_clicked)
def plot_graph(starts_at=0, ends_at=2*np.pi):
    f = lambda x : sum(1/a*np.sin(a*x + np.pi/a) for a in range(1,6))
    x = np.linspace(0, 2*np.pi, 1000)
    plt.plot(x, f(x))
    plt.xlim([starts_at, ends_at])

display(widgets.VBox([start_slider, end_slider, button]))
widgets.interactive_output(plot_graph, {'starts_at': start_slider, 'ends_at':end_slider})

但是,这将在您每次更新时完全重新生成情节,这可能会导致体验不稳定。因此,如果您在笔记本中使用交互式 matplotlib 后端,也可以 re-write 使用类似 .set_data 的 matplotlib 方法。所以如果你要使用 ipympl you could follow the examples in this example notebook.

通过另一个图书馆

我写了一个库mpl-interactions to make it easier to control matplotlib plots using ipywidgets sliders. It provides a function analogous to ipywidgets.interact in that it handles creating the widgets for you, but it has the advantage of being matplotlib focused so all you need to provide is the data. More about the differences to ipywidgets here

%matplotlib ipympl
import mpl_interactions.ipyplot as iplt
import matplotlib.pyplot as plt
import numpy as np
import ipywidgets as widgets

def plot_graph(starts_at=0, ends_at=2*np.pi):
    x = np.linspace(starts_at, ends_at, 1000)
    f = lambda x : sum(1/a*np.sin(a*x + np.pi/a) for a in range(1,6))
    return np.array([x, f(x)]).T


fig, ax = plt.subplots()
button = widgets.Button(description = 'reset')
display(button)
controls = iplt.plot(plot_graph, starts_at = (0, np.pi), ends_at = (np.pi, 2*np.pi), xlim='auto', parametric=True)
def on_click(event):
    for hbox in controls.controls.values():
        slider = hbox.children[0]
        slider.value = slider.min
button.on_click(on_click)

根据@iperetta 的回答,您可以创建一个简单的装饰器,为每次使用添加一个重置按钮:

按照上面的回答重置按钮:

def reset_button(defaults={}):
    def on_button_clicked(_):
        for k, v in defaults.items(): 
            k.value = v
    button = widgets.Button(description='Reset')
    button.on_click(on_button_clicked)
    display(button)

装饰者:

def interact_plus_reset(**_widgets):
  default_vals = {wid:wid._trait_values['value'] for k, wid in _widgets.items()}
  reset_button(defaults=default_vals)
  def wrap(func):
    @wraps(func)
    @widgets.interact(**_widgets)
    def inner(*args, **kwargs):
      return func(*args, **kwargs)
    return inner
  return wrap

然后如下使用:

@interact_plus_reset(
  a = widgets.FloatSlider(min=1, max=2000, value=35, step=1), 
  b = widgets.FloatSlider(min=1, max=2000, value=1000, step=1), 
)
def run(a, b):
  print(a + b)