低层级部件详解#
它们如何融入整体画面?#
Jupyter Notebook 的目标之一是最大程度地缩短用户与其数据之间的“距离”。这意味着允许用户快速查看和操作数据。
在小部件之前,这仅仅是代码分段以及执行这些分段后产生的结果。 |
小部件通过允许用户界面交互直接操作内核中的数据,进一步缩短了用户与其数据之间的距离。 |
如何实现?#
Jupyter交互式小部件是交互式元素,比如滑块、文本框、按钮,它们在核心(代码执行的地方)和前端(Notebook网页界面)都有表示。为此,必须存在一个清晰、高度抽象化的通信层。
通信#
这里是Jupyter notebook "通信机制"发挥作用的地方。通信API是一个对称的、异步的、即发即弃风格的消息传递API。它允许程序员在前端和后端之间发送可JSON序列化的数据块。通信API隐藏了web服务器、ZMQ和WebSocket的复杂性。
同步状态#
使用通信机制,小部件基础层旨在保持状态同步。在内核中,存在一个小部件实例。此小部件实例在前端有对应的小部件模型实例。小部件和小部件模型存储相同的状态。小部件框架确保两个模型彼此保持同步。如果小部件模型在前端发生更改,内核中的小部件也会接收到相同的更改。反之,如果内核中的小部件发生更改,前端的小部件模型也会接收到相同的更改。不存在单一事实来源,两个模型具有相同的优先级。尽管笔记本具有单元格的概念,但小部件或小部件模型都不绑定到任何单个单元格。
模型和视图#
为了让用户能在逐个单元格的基础上与控件进行交互,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)
代码执行#
显示简单 FloatSlider 小组件所需的用户代码为:
from ipywidgets import FloatSlider
from IPython.display import display
slider = FloatSlider()
display(slider)
为了理解小部件是如何显示的,必须了解在Notebook中代码是如何执行的。执行从代码单元格开始。用户事件触发代码单元格向内核发送评估代码消息,包含代码单元格中的所有代码。这个消息会被分配一个GUID,前端将其与代码单元格关联并记住它(重要)。
一旦内核接收到该消息,内核会立即向前端发送一条"工作中"状态消息。随后内核继续执行代码。
模型构建#
当在核心中构建一个Widget时,首先发生的是构建一个comm并将其与widget关联。当comm构建时,会分配一个GUID(全局唯一标识符)。一个comm-open消息被发送到前端,附带元数据说明该comm是一个widget comm以及对应的WidgetModel类是什么。
WidgetModel 类通过模块和名称进行指定。随后使用 Require.js 异步加载 WidgetModel 类。该消息触发前端创建一个与后端具有相同 GUID 的通信器。然后,新的通信器被传递到前端的 WidgetManager 中,该管理器创建 WidgetModel 类的一个实例,并与通信器相关联。Widget 和 WidgetModel 都将通信器的 GUID 重新用作自己的 GUID。
异步地,内核在发送comm-open消息后立即向前端发送一个初始状态推送,其中包含Widget的初始状态。在WidgetModel构建时,此状态消息可能已被接收,也可能未被接收。无论如何,该消息会被缓存,并在WidgetModel构建完成后进行处理。初始状态推送使得前端中的WidgetModel与内核中的Widget保持同步。
显示视图#
在 Widget 构建完成之后,它可以被显示。调用 display(widgetinstance) 会触发 widget 中一个特殊命名的 repr 方法。该方法会向前端发送一条消息,通知前端构建并显示一个 widget 视图。这条消息是对原始代码执行消息的响应,原始消息的 GUID 被存储在新消息的头部。当前端接收到消息时,它会使用原始消息的 GUID 来确定新视图应属于哪个单元格。然后,使用 WidgetModel 状态中指定的 WidgetView 类创建视图。同样的 require.js 方法用于加载视图类。一旦类加载完成,将构建其实例,显示在正确的单元格中,并注册对模型变更的监听器。
小部件骨架#
%%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);
由于小部件同时存在于前端和内核中,它们由Python(如果内核是IPython)和Javascript代码组成。下面展示了一个基础的小部件示例:
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:
基础小部件类是DOMWidget和Widget。DOMWidget类表示在页面中以HTML DOM元素形式呈现的小部件。Widget类更为通用,可用于不一定作为DOM元素存在于页面上的对象(例如,继承自Widget的小部件可能表示一个Javascript对象)。
_view_module, _view_module_version 和 _view_name 是前端知道为模型构建什么视图类的方式。
sync=True 是使特征量表现如状态的原因。
类似命名的 _model_module, _model_module_version, 和 _model_name 可用于指定对应的 WidgetModel。
count 是一个自定义状态片的示例。
描述JavaScript:
define 调用异步加载指定的依赖项,然后将它们作为参数传递给回调函数。在这里,加载的唯一依赖项是基本小部件模块。
自定义视图继承自DOMWidgetView或WidgetView。DOMWidgetView类用于将自身渲染到DOM元素中的小部件,而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 类型如 int, float, list 和 dict 仅映射为 Number, Array 和 Object。对于更复杂的类型,必须在 Python 端和 JavaScript 端指定序列化器和反序列化器。
Python 端的自定义序列化与反序列化#
在许多情况下,必须为trait属性指定自定义序列化。例如,
如果特征属性不可进行 JSON 序列化
如果特质属性包含JavaScript端不需要的数据。
可通过元数据中的to_json和from_json为指定特征属性自定义序列化。这两个参数必须是接收两个参数的函数
需要被[反]序列化的值
底层widget模型的实例。
在大多数情况下,序列化器的实现中不会使用第二个参数。
示例
例如,对于 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数据结构。
微件模型之间的父子关系案例
当一个widget模型包含其他widget模型时,必须使用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必须存在于内核中,内核是小部件安装的自然场所。然而,截至目前,内核不托管静态资源。相反,静态资源由网络服务器托管,这是位于内核和前端之间的实体。这是一个问题,因为这意味着小部件具有需要同时安装在网络服务器和内核中的组件。内核组件易于安装,因为您可以依赖语言的内置工具。网络服务器的静态资源使事情变得复杂,因为需要一个额外的步骤来让网络服务器知道资源的位置。
静态资源#
在经典的Jupyter notebook中,静态资源通过Jupyter扩展的形式提供给Jupyter notebook。JavaScript包被复制到一个可通过nbextensions/处理器访问的目录中。Nbextensions还有一个在页面加载时运行代码的机制。这可以使用install-nbextension命令进行设置。
分布#
有两个模板项目以cookiecutters的形式提供:
JavaScript: https://github.com/jupyter-widgets/widget-cookiecutter
TypeScript:https://github.com/jupyter-widgets/widget-ts-cookiecutter
这些cookiecutters旨在帮助自定义小部件作者开始打包和分发Jupyter交互式小部件。
他们按照当前使用交互式小部件的最佳实践,为一个小部件库制作了一个项目。提供了一个占位符“Hello World”小部件的实现。