低级小部件解释#

它们是如何融入整体的?#

Jupyter Notebook 的目标之一是最小化用户与数据之间的“距离”。这意味着允许用户快速查看和操作数据。

inputoutput

example-widgets

在引入小部件之前,这只是代码段和执行这些代码段的结果的分割。

通过允许用户通过 UI 交互直接在内核中操作数据,小部件进一步减小了用户与数据之间的距离。

How?#

Jupyter交互式小部件是交互元素,如滑块、文本框、按钮等,它们在内核(代码执行的地方)和前端(Notebook网络界面)都有表示。为此,必须存在一个干净、良好抽象的通信层。

Comms#

这就是Jupyter notebook的“comms”发挥作用的地方。comm API是一个对称的、异步的、无需等待的消息传递API。它允许程序员在前端和后端之间发送可JSON化的数据块。comm API隐藏了web服务器、ZMQ和websockets的复杂性。

comms

使用 comms,小部件基础层旨在保持状态同步。在内核中存在一个 Widget 实例。这个Widget实例在前端有一个对应的WidgetModel实例。Widget和WidgetModel存储相同的状态。小部件框架确保两个模型彼此保持同步。如果在前端的 WidgetModel 发生变化,内核中的Widget会接收到相同的变化。反之亦然,如果内核中的Widget发生变化,前端的WidgetModel会接收到相同的变化。没有一个单一的真理来源,两个模型具有相同的优先级。尽管笔记本有单元格的概念,但Widget或WidgetModel都不绑定到任何单个单元格。

synchronized state

模型和视图#

为了让用户能够逐个单元格地与小部件进行交互,WidgetModels由WidgetViews表示。任何单个WidgetView都绑定到单个单元格。多个WidgetViews可以链接到一个单独的WidgetModel。这就是你可以多次重新显示同一个小部件并且它仍然有效的原因。为了实现这一点,小部件框架使用了Backbone.js。在传统的MVC框架中,WidgetModel是(M)odel,而WidgetView既是(V)iew又是(C)ontroller。这意味着,视图既显示模型的状态也操作它。想象一下滑块控件,它既显示值又允许用户通过拖动滑块来改变值。

from ipywidgets import *
from IPython.display import display
w = IntSlider()
display(w, w)
display(w)

model-view venn diagram

代码执行#

要在Notebook中显示一个简单的FloatSlider小部件,用户代码如下:

from ipywidgets import FloatSlider
from IPython.display import display
slider = FloatSlider()
display(slider)

要理解如何在Notebook中显示一个小部件,必须了解代码在Notebook中的执行方式。执行从代码单元格开始。用户事件触发代码单元格向内核发送一个评估代码消息,包含代码单元格中的所有代码。这个消息被赋予一个GUID,前端将其与代码单元格关联,并记住它(重要)。

execution-1

一旦内核接收到该消息,内核会立即向前端发送一个“我正忙”状态消息。然后内核继续执行代码。

execution-2

Model 限制#

当在内核中构造一个Widget时,首先发生的是构造一个comm并将其与小部件关联。当构造comm时,会给它一个GUID(全局唯一标识符)。然后向前端发送一个comm-open消息,其中包含元数据,说明这个comm是一个widget comm以及相应的WidgetModel类是什么。

model-construction

WidgetModel 类由模块和名称指定。然后使用Require.js异步加载WidgetModel类。该消息触发在前端创建一个具有与后端相同GUID的comm。然后,新的comm被传递到前端的WidgetManager中,后者创建了与comm关联的WidgetModel类的实例。Widget和WidgetModel都将comm GUID作为自己的用途。

construction-2

在异步地,内核在comm-open消息之后立即向前端发送一个初始状态推送,包含Widget的初始状态。这个状态消息可能会或不会被WidgetModel构造时接收到。无论如何,该消息都会被缓存并在WidgetModel构造完成后处理。初始状态推送是导致前端的WidgetModel与内核中的Widget同步的原因。

construction-3

展示视图#

在Widget构建完成之后,它可以被显示出来。调用display(widgetinstance)会触发widget中一个特殊命名的repr方法。这个方法会向前端发送一条消息,告诉前端去构建并显示一个widget视图。这条消息是对原始代码执行消息的响应,而原始消息的GUID被存储在新消息的头部。当前端接收到这个消息时,它使用原始消息的GUID来确定新视图应该属于哪个单元格。然后,使用WidgetModel状态中指定的WidgetView类来创建视图。同样的require.js方法被用来加载视图类。一旦类被加载,就构建它的一个实例,在正确的单元格中显示,并为模型的变化注册监听器。

display a view

Widget skeleton#

%%javascript
this.model.get('count');
this.model.set('count', 999);
this.touch();

/////////////////////////////////

this.colorpicker = document.createElement('input');
this.colorpicker.setAttribute('type', 'color');
this.el.appendChild(this.colorpicker);

由于widgets同时存在于前端和内核中,它们由Python(如果内核是IPython的话)和JavaScript代码组成。下面可以看到一个样板widget的示例:

Python:

from ipywidgets import DOMWidget
from traitlets import Unicode, Int
 
class MyWidget(DOMWidget):
	_view_module = Unicode('mywidget').tag(sync=True)
    _view_module_version = Unicode('0.1.0').tag(sync=True)
	_view_name = Unicode('MyWidgetView').tag(sync=True)
	count = Int().tag(sync=True)

JavaScript:

define('mywidget', ['@jupyter-widgets/base'], function(widgets) {
	var MyWidgetView = widgets.DOMWidgetView.extend({
    	render: function() {
        	MyWidgetView.__super__.render.apply(this, arguments);
        	this._count_changed();
        	this.listenTo(this.model, 'change:count', this._count_changed, this);
    	},
 
    	_count_changed: function() {
        	var old_value = this.model.previous('count');
        	var new_value = this.model.get('count');
        	this.el.textContent = String(old_value) + ' -> ' + String(new_value);
    	}
	});
 
	return {
    	MyWidgetView: MyWidgetView
	}
});

描述Python:

基础的widget类是DOMWidgetWidgetDOMWidget类代表在页面中以HTML DOM元素形式呈现的widget。Widget类更为通用,可以用于可能不在页面上作为DOM元素存在的对象(例如,继承自Widget的widget可能代表一个JavaScript对象)。

_view_module_view_module_version_view_name是前端知道为模型构建哪个视图类的方式。

sync=True使得traitlets表现得像状态。

类似命名的_model_module_model_module_version_model_name可以用来指定相应的WidgetModel。

count是一个自定义状态的例子。

描述的

define调用异步加载指定的依赖项,然后将它们作为参数传递给回调函数。在这里,唯一加载的依赖项是基础widget模块。

自定义视图继承自DOMWidgetViewWidgetViewDOMWidgetView类用于将自己渲染为DOM元素的widget,而WidgetView类没有这个假设。

自定义模型继承自WidgetModel

render方法是用于呈现视图内容的方法。如果视图是DOMWidgetView,则.el属性包含将在页面上显示的DOM元素。

.listenTo允许视图监听模型属性的变化。

_count_changed是一个可以用来处理模型变化的方法的例子。

this.model是可以用来访问相应模型的方式。

this.model.previous将获取特性的前一个值。

this.model.get将获取特性的当前值。

this.model.set后面跟着this.model.save_changes();改变模型。使用视图方法touch而不是model.save_changes来关联更改与当前视图,从而将任何响应消息与视图的单元格关联起来。

返回的字典是模块的公共成员。

小部件属性的序列化#

带有sync=True的小部件特性属性在JavaScript端与JavaScript模型实例同步。因此,它们需要被序列化为json

默认情况下,基本的Python类型,如intfloatlistdict只是简单地映射到NumberArrayObject。对于更复杂的类型,必须在Python端和JavaScript端指定序列化器和反序列化器。

Python端的自定义序列化和反序列化#

在许多情况下,必须为特性属性指定自定义序列化。例如,

  • 如果特性属性不可序列化为json

  • 如果特性属性包含JavaScript端不需要的数据。

可以通过to_jsonfrom_json元数据为给定的特性属性指定自定义序列化。这些必须是接受两个参数的函数

  • 要[反]序列化的值

  • 底层小部件模型的实例。

在大多数情况下,实现序列化器时不使用第二个参数。

示例

例如,在DatePicker小部件的value属性的情况下,声明如下:

value = Datetime(None, allow_none=True).tag(sync=True, to_json=datetime_to_json, from_json=datetime_from_json)

其中datetime_to_json(value, widget)datetime_from_json(value, widget)返回或处理适合前端的json数据结构。

小部件模型之间的父子关系

当一个小部件模型包含其他小部件模型时,必须使用ipywidgets打包到widget_serialization字典中的序列化器和反序列化器。

例如,HBox小部件以以下方式声明其children属性:

from .widget import widget_serialization

[...]

children = Tuple().tag(sync=True, **widget_serialization)

实际的一个小部件模型序列化结果是包含前缀为"IPY_MODEL_"的小部件id的字符串。

JavaScript端的自定义序列化和反序列化#

为了镜像Python端的自定义序列化器和反序列化器,JavaScript端必须提供对称的方法。

在JavaScript端,通过小部件模型的serializers类级属性指定序列化器。

它们通常以以下方式指定,扩展基类的序列化器字典。在下面的示例中,来自DatePicker的反序列化器为value属性指定了。

static serializers = _.extend({
    value: {
        serialize: serialize_datetime,
        deserialize: deserialize_datetime
    }
}, BaseModel.serializers)

自定义序列化器是接受两个参数的函数:要[反]序列化的对象的值和小部件管理器。在大多数情况下,实际上并不使用小部件管理器。

安装#

因为任何给定小部件的API 必须存在于内核中,所以内核是安装小部件的自然位置。然而,目前的内核不托管静态资源。相反,静态资源由web服务器托管,web服务器是位于内核和前端之间的实体。这是一个问题,因为这意味着小部件有需要 同时在web服务器和内核中安装的组件。内核组件很容易安装,因为你可以使用语言的内置工具。web服务器的静态资源使事情变得复杂,因为需要额外的步骤来让web服务器知道资源在哪里。

静态 assets#

在经典的Jupyter笔记本中,静态资源以Jupyter扩展的形式提供。JavaScript包被复制到一个可以通过nbextensions/处理器访问的目录中。Nbextensions还有一个在页面加载时运行你的代码的机制。这可以通过install-nbextension命令来设置。

分发#

两个模板项目以cookiecutters的形式提供:

  • https://github.com/jupyter-widgets/widget-cookiecutter

  • TypeScript: https://github.com/jupyter-widgets/widget-ts-cookiecutter

这些cookiecutters旨在帮助自定义小部件作者开始打包和分发Jupyter交互式小部件。

它们产生了一个遵循当前最佳实践使用交互式小部件的小部件库的项目。提供了一个占位符“Hello World”小部件的实现。