异步小部件#

本笔记本涵盖了两种情况,我们希望与小部件相关的代码在运行时不会阻塞内核处理其他执行请求:

  1. 暂停代码以等待用户在前端与小部件进行交互

  2. 在后台更新小部件

等待用户交互#

你可能希望暂停你的 Python 代码,等待用户在前端与小部件进行一些交互。通常这很难实现,因为运行 Python 代码会阻塞任何来自前端的小部件消息,直到 Python 代码执行完毕。

我们将通过两种方法来实现这一点:使用事件循环集成和使用普通的生成器函数。

事件循环集成#

如果我们利用 IPython 提供的事件循环集成,我们可以使用 Python 3 中的 async/await 语法来实现一个很好的解决方案。

首先,我们需要调用我们的异步 io 事件循环。这需要 ipykernel 4.7 或更高版本。

%gui asyncio

我们定义一个新函数,当小部件属性发生变化时,该函数返回 future 对象。

import asyncio
def wait_for_change(widget, value):
    future = asyncio.Future()
    def getvalue(change):
        # make the new value available
        future.set_result(change.new)
        widget.unobserve(getvalue, value)
    widget.observe(getvalue, value)
    return future

最终我们得到了一个函数,在这个函数中我们将等待小部件的变化。我们将做10个单位的工作,并在每个工作之后暂停,直到我们观察到小部件发生变化。请注意,小部件的值对我们来说是可用的,因为它是 wait_for_change future的结果。

运行这个函数,并更改滑块10次。

from ipywidgets import IntSlider, Output
slider = IntSlider()
out = Output()

async def f():
    for i in range(10):
        out.append_stdout('did work ' + str(i) + '\n')
        x = await wait_for_change(slider, 'value')
        out.append_stdout('async function continued with value ' + str(x) + '\n')
asyncio.ensure_future(f())

slider
# out 取消注释,查看结果

生成器方法#

如果你不能利用 async/await 语法,或者你不想修改事件循环,你也可以使用生成器函数来实现。

首先,我们定义一个装饰器,它将生成器函数与小部件更改事件关联起来。

from functools import wraps
def yield_for_change(widget, attribute):
    """Pause a generator to wait for a widget change event.
        
    This is a decorator for a generator function which pauses the generator on yield
    until the given widget attribute changes. The new value of the attribute is
    sent to the generator and is the value of the yield.
    """
    def f(iterator):
        @wraps(iterator)
        def inner():
            i = iterator()
            def next_i(change):
                try:
                    i.send(change.new)
                except StopIteration as e:
                    widget.unobserve(next_i, attribute)
            widget.observe(next_i, attribute)
            # start the generator
            next(i)
        return inner
    return f

然后我们设置我们的生成器。

from ipywidgets import IntSlider, VBox, HTML
slider2=IntSlider()

@yield_for_change(slider2, 'value')
def f():
    for i in range(10):
        print('did work %s'%i)
        x = yield
        print('generator function continued with value %s'%x)
f()

slider2
did work 0

修改#

上述两种方法都在等待小部件更改事件,但可以修改为等待其他事件,比如按钮事件消息(如“继续”按钮)等。

在后台更新小部件#

有时你可能希望在后台更新小部件,允许内核同时处理其他执行请求。我们可以使用线程来实现这一点。在下面的例子中,进度条将在后台更新,并允许主内核进行其他计算。

import threading
from IPython.display import display
import ipywidgets as widgets
import time
progress = widgets.FloatProgress(value=0.0, min=0.0, max=1.0)

def work(progress):
    total = 100
    for i in range(total):
        time.sleep(0.2)
        progress.value = float(i+1)/total

thread = threading.Thread(target=work, args=(progress,))
display(progress)
thread.start()