这里写目录标题
作者:Dreamstar 校对:Dreamstar, Lan
内容来源:这个文档中的内容来源于orange软件的小部件开发指引的翻译。
相关链接:https://orange-widget-base.readthedocs.io/en/latest/#
准备开始 - Getting Started
Orange Widgets是Orange的可视化编程环境Orange Canvas中的组件。它们代表了一些自带功能,并提供图形用户界面(GUI)。小部件(Widgets)之间相互通信,并通过通信通道传递对象,以便从左到右与其他小部件交互。
在这个部分,我们将从一些简单的要点开始,向您展示如何构建一个简单的小部件,并使该小部件可以在Orange Canvas中运行。
先决条件 - Prerequisites
每个Orange小部件都属于一个类别,并且在该类别中具有相关的优先级。打开Orange Canvas(Orange附带的可视化编程环境)时,小部件列在左侧的工具箱中:
每个小部件都有一个名称描述和一组输入/输出(称为小部件的元描述)。
这些元数据是在Orange Canvas应用程序启动时发现,该应用程序利用了setuptools/displit及其入口点协议。Orange Canvas使用Orange.widgets
入口点查找小部件。
Defining a widget - 定义小部件
OWBaseWidget
是Orange Canvas工作流中小部件的基类。
画布框架中的每个小部件都需要定义其元数据。这包括小部件的名称和文本描述,更重要的是它的输入/输出规范。这是通过在小部件的类命名空间中定义常量来实现的。
我们将从一个非常简单的例子开始。小部件的功能为:输出用户指定的单个整数。
from orangewidget.widget import OWBaseWidget, Output
from orangewidget.settings import Setting
from orangewidget import gui
class IntNumber(OWBaseWidget):
# Widget's name as displayed in the canvas
name = "Integer Number"
# Short widget description
description = "Lets the user input a number"
# An icon resource file path for this widget
# (a path relative to the module where this widget is defined)
icon = "icons/number.svg"
# Widget's outputs; here, a single output named "Number", of type int
class Outputs:
number = Output("Number", int)
根据设计原则,Orange小部件的主界面通常被划分为控制区域和主区域。控制区域出现在左侧,应该包括一些选项/设置。主区域通常包括一个图形、表格或一些绘图,这些图形、表格和绘图基于小部件的输入和控制区域中的选项/设置。OWBaseWidget
通过其属性self.controlArea和self.mainArea使这两个区域可用。注意,虽然所有小部件都有这种通用的视觉外观是最好的,但您仍可以以任何方式使用这些区域,甚至忽略其中一个区域,使用与Orange中其他小部件不同的风格来组成您的小部件。
我们使用类属性标志指定默认布局。在这里,我们将只使用单列(controlArea)GUI。
# Basic (convenience) GUI definition:
# a simple 'single column' GUI layout # 简单的单列GUI布局
want_main_area = False
# with a fixed non resizable geometry. # 固定且不可调整大小
resizing_enabled = False
我们希望在保存/加载工作流时保存/恢复用户当前输入的数字。我们可以通过在小部件的类定义中声明一个特殊的属性/成员来实现这一点,如下所示:
number = Setting(42)
最后,定义GUI和相关小部件功能的实际代码如下:
def __init__(self):
super().__init__()
from AnyQt.QtGui import QIntValidator
gui.lineEdit(self.controlArea, self, "number", "Enter a number",
box="Number",
callback=self.number_changed,
valueType=int, validator=QIntValidator())
self.number_changed()
def number_changed(self):
# Send the entered number on "Number" output
self.Outputs.number.send(self.number)
更多内容见:
orangewidget.gui.lineEdit()
,
但是这时小部件目前是无法使用的,因为没有小部件来接受它的输出。因此,让我们定义一个显示数字的小部件。
from orangewidget.widget import OWBaseWidget, Input
from orangewidget import gui
class Print(OWBaseWidget):
name = "Print"
description = "Print out a number"
icon = "icons/print.svg"
class Inputs:
number = Input("Number", int)
want_main_area = False
def __init__(self):
super().__init__()
self.number = None
self.label = gui.widgetLabel(self.controlArea, "The number is: ??")
@Inputs.number
def set_number(self, number):
"""Set the input number."""
self.number = number
if self.number is None:
self.label.setText("The number is: ??")
else:
self.label.setText("The number is {}".format(self.number))
我们用类Inputs定义输入,就像输出需要由Outputs定义一样。但是,每个输入都必须由一个类方法处理。我们通过装饰器来标记;在上述情况下,将*@Inputs.number放在方法的定义之前。
请注意,在set_number方法中,我们检查数字是否为None*。当小部件之间的连接被删除时,或者如果我们所连接的发送小部件故意清空了通道,则会向小部件发送None。
现在,我们可以使用一个小部件来输入数字,另一个来显示它。
再一个:
from orangewidget.widget import OWBaseWidget, Input, Output
class Adder(OWBaseWidget):
name = "Add two integers"
description = "Add two numbers"
icon = "icons/add.svg"
class Inputs:
a = Input("A", int)
b = Input("B", int)
class Outputs:
sum = Output("A + B", int)
want_main_area = False
def __init__(self):
super().__init__()
self.a = None
self.b = None
@Inputs.a
def set_A(self, a):
"""Set input 'A'."""
self.a = a
@Inputs.b
def set_B(self, b):
"""Set input 'B'."""
self.b = b
def handleNewSignals(self):
"""Reimplemeted from OWBaseWidget."""
if self.a is not None and self.b is not None:
self.Outputs.sum.send(self.a + self.b)
else:
# Clear the channel by sending `None`
self.Outputs.sum.send(None)
See also:
handleNewSignals()
在学习了什么是Orange Widget以及如何在玩具示例中定义之后,我们将构建一个比较有用的小部件,这些小部件可以与现有的Orange widgets一起工作。
我们将从一个非常简单的例子开始,它将在输入上接收一个数据集,并输出一个包含10%数据实例的数据集。我们将把这个小部件称为OWDataSamplerA(OW代表Orange widget,DataSampler的命名是因为,这是小部件将要做的事情,而A因为我们在教程中对一些小部件进行了原型化)。
一个“示例”包 - A ‘Demo’ package
首先,为了将我们的新小部件插入到在Orange Canvas的工具箱中,我们将创建一个名为Orange demo的虚拟python项目。创建Orange插件的一个很好的起点是示例插件。
文件布局如下:
orange-demo/
setup.py
orangedemo/
__init__.py
OWDataSamplerA.py
orange-demo/setup.py的内容应该包含:
from setuptools import setup
setup(name="Demo",
packages=["orangedemo"],
package_data={"orangedemo": ["icons/*.svg"]},
classifiers=["Example :: Invalid"],
# Declare orangedemo package to contain widgets for the "Demo" category
entry_points={"orange.widgets": "Demo = orangedemo"},
)
请注意,我们声明我们的orangedemo包包含来自特定定义类别Demo的小部件。
另请阅读:https://github.com/biolab/orange3/wiki/Add-Ons
根据前面的示例,我们定义OWDataSamplerA小部件的模块开始如下:
import numpy
import Orange.data
from orangewidget.widget import OWBaseWidget, Input, Output
from orangewidget.utils.widgetpreview import WidgetPreview
from orangewidget import gui
class OWDataSamplerA(OWBaseWidget):
name = "Data Sampler"
description = "Randomly selects a subset of instances from the dataset"
icon = "icons/DataSamplerA.svg" # mf-需自行准备svg文件
priority = 10
class Inputs:
data = Input("Data", Orange.data.Table)
class Outputs:
sample = Output("Sampled Data", Orange.data.Table)
want_main_area = False
def __init__(self):
super().__init__()
# GUI
box = gui.widgetBox(self.controlArea, "Info")
self.infoa = gui.widgetLabel(box, 'No data on input yet, waiting to get something.')
self.infob = gui.widgetLabel(box, '')
该小部件定义了一个输入通道“Data”和一个输出通道“Sampled Data”。两者都将携带Orange.data.Table
.类型的令牌。在代码中,我们将把信号称为Inputs.data和Outputs.sample。
通道可以携带任意类型的令牌。然而,小部件的目的是与其他小部件信息交流,因此作为主要设计原则之一,我们试图通过最小化不同通道类型的数量来最大限度地提高小部件的灵活性。在检查是否能重用现有信号之前,不要发明新的信号类型。
由于我们的小部件除了一些信息外不会其他显示任何内容,因此我们将把这两个标签放在控制区域,并用“Info”框将其包裹。
接下来的四行指定了小部件的GUI。这将很简单,并且将只包括两行文本,如果什么都不发生,第一行将报告“no data yet - 还没有数据”,第二行将为空。
为了完成我们的小部件,我们现在需要定义一个处理输入数据的方法。我们将其称为set_data()
;名称是任意的,但调用方法set_<name of the input>
似乎是一种很好的做法。为了将其指定为接受Inputs.data
中定义的信号的方法,我们用*@Inputs.data*对其进行修饰。
@Inputs.data
def set_data(self, dataset):
if dataset is not None:
self.infoa.setText('%d instances in input dataset' % len(dataset))
indices = numpy.random.permutation(len(dataset))
indices = indices[:int(numpy.ceil(len(dataset) * 0.1))]
sample = dataset[indices]
self.infob.setText('%d sampled instances' % len(sample))
self.Outputs.sample.send(sample)
else:
self.infoa.setText('No data on input yet, waiting to get something.')
self.infob.setText('')
self.Outputs.sample.send("Sampled Data")
dataset
参数是通过我们方法需要处理的来自于输入通道的令牌。
为了处理非空令牌,小部件更新接口报告输入上的数据数量,然后使用Orange进行数据采样,并更新接口报告采样实例数量。最后,采样数据作为令牌发送到定义为Output.sample的输出通道。
虽然我们的小部件现在已经准备好测试了,但最后,让我们为我们的小组件设计一个图标。正如小部件标题中所指定的,我们将称之为DataSamplerA.svg,并将其放在orange-demo目录的icons子目录中。
有了目前的文件素材,我们现在可以开始安装orangedemo软件包了。我们将通过运行pip install -e .
来实现这一点。命令。
pip install -e .
注意:
上面的pip安装命令仅仅只是将插件注册到了orange软件中,但是代码依旧保留在开发目录中(即没有将其复制到python的site-packages中)
根据您的python安装环境,您可能需要管理员/超级用户权限。
为了进行测试,我们现在打开Orange Canvas。小部件工具箱中应该有一个名为Demo的新窗格。如果我们点击这个窗格,它会显示我们的小部件的图标。试着将鼠标悬停在它上面,看看标题和频道信息是否处理正确:
现在进行真正的测试。我们将File小部件放在画布上(从Data窗格),并加载iris.tab数据集。我们还将数据采样器小部件放在画布上并打开它(双击图标,或右键单击并选择打开):
现在连接“文件”和“数据采样器”小部件(单击“文件”小部件的输出连接器,并将行拖动到“数据采样器》的输入连接器)。如果一切正常,只要释放鼠标,连接就会建立,并且等待文件小部件输出的令牌会被发送到数据采样器小部件,后者会更新其窗口:
要查看数据采样器是否确实向输出发送了一些数据,请将其连接到数据表小部件:
尝试打开不同的数据文件(更改应该通过小部件连接传递数据,打开“数据表”窗口后,您应该立即看到采样结果)。同时尝试删除“文件”和“数据采样器”之间的连接(右键单击连接,选择“删除”)。查看数据表中显示的数据会发生什么变化?
在Orange Canvas外测试小工具 - Testing Your Widget Outside Orange Canvas
出于调试目的,我们希望能够独立运行小部件:如果包含小部件代码的文件作为主脚本执行,它应该显示小部件并向其提供一些合适的数据。最简单的方法是使用oragewidget.utils.WidgetPreview并将默认信号的数据传递给它。
if __name__ == "__main__":
WidgetPreview(OWDataSamplerA).run(Orange.data.Table("iris"))
教程(设置和控件)- Tutorial (Settings and Controls)
在本教程的前一节中,我们刚刚构建了一个简单的采样小部件。现在让我们通过允许用户设置要保留在样本中的数据实例的比例,使这个小部件更加有用。假设我们想设计一个看起来像这样的小部件:
我们添加的是一个选项框(Options box),其中有一个用于设置样本大小的输入框,以及一个用于提交(发送)我们在设置中所做的任何更改的复选框和按钮。如果选中了“Commit data on selection change”复选框,则样本大小的任何变化都将使小部件发送采样的数据集。如果数据集很大(比如数千个或更多),我们可能只想在设置完样本大小后发送样本数据,因此我们不选中提交复选框,并在准备好后按“提交”。
这是一个非常简单的界面,但它还有更多的功能。我们希望设置可以被保存(样本大小和提交按钮的状态)。也就是说,我们所做的任何更改,我们都希望保存它,以便下次打开小部件时,设置就在那里。
部件设置 - Widgets Settings
幸运的是,由于我们使用了基类OWBaseWidget
,所以设置可以很好的被处理。我们只需要告诉哪些变量将用于持久设置。
在我们的小部件中,我们将使用两个设置变量,并在小部件类定义中声明这一点(在输入、输出定义之后)。
class OWDataSamplerB(OWBaseWidget):
name = "Data Sampler (B)"
description = "Randomly selects a subset of instances from the dataset."
icon = "icons/DataSamplerB.svg"
priority = 20
class Inputs:
data = Input("Data", Orange.data.Table)
class Outputs:
sample = Output("Sampled Data", Orange.data.Table)
proportion = settings.Setting(50)
commitOnChange = settings.Setting(0)
所有设置都必须指定其默认值。创建小部件时,小部件的成员已经恢复,并准备在其__init__方法中使用。两个变量(self.proportion
和self.commitOnChange
)的内容将在关闭我们的小部件时保存。在我们的小部件中,我们不会直接设置这些变量,而是将它们与GUI控件一起使用。
控制和模块的GUI - Controls and module gui
我们将使用**orangewidget.gui
**来创建/定义gui。有了这个库,选项框的GUI定义部分会有点密集,但非常简洁。
box = gui.widgetBox(self.controlArea, "Info")
self.infoa = gui.widgetLabel(box, 'No data on input yet, waiting to get something.')
self.infob = gui.widgetLabel(box, '')
gui.separator(self.controlArea)
self.optionsBox = gui.widgetBox(self.controlArea, "Options")
gui.spin(self.optionsBox, self, 'proportion',
minv=10, maxv=90, step=10, label='Sample Size [%]:',
callback=[self.selection, self.checkCommit])
gui.checkBox(self.optionsBox, self, 'commitOnChange',
'Commit data on selection change')
gui.button(self.optionsBox, self, "Commit", callback=self.commit)
self.optionsBox.setDisabled(True)
我们已经熟悉了第一部分-信息框。为了使小部件更好,我们在信息框和选项框之间放置了一个分隔符。在定义了选项框之后,这里是我们的第一个严格意义上的gui控件:orangewidget.gui.spin()
。第一个参数指定其父窗口小部件/布局,在本例中为self.optionsBox
(生成的窗口小部件对象将自动将自己附加到父窗口小部件的布局)。第二个(self)和第三个(“proportion”)定义了数值微调框(spin box)的属性绑定。也就是说,数值微调框(spin box)控件中的任何更改都将自动传播到self.proportions
,反之亦然——通过赋值更改小部件代码中self.proprotions的值(例如self.proprotions = 30)将更新数值微调框(spin box)的状态以匹配。
数值微调框(spin box)的其余部分为控件提供了一些参数(最小值、最大值和步长),传递给放置在顶部的标签(label),以及当数值微调框(spin box)中的值更改时要调用哪个函数。我们需要第一个回调来制作数据样本,并在“信息框“中报告样本的大小,第二个回调用来检查我们是否可以发送这些数据。在orangewidget.gui中,回调要么是对函数的引用,要么是带有引用的列表,就像我们的例子一样。
通过以上所有操作,orangewidget.gui.checkBox()
的调用参数也应该是清楚的。请注意,这里和对orangewidget.gui.spin()
的调用不需要告诉控件初始化值的参数:构造时,两个控件都已经为相关变量设置了包含的值。
就是这样。请注意,作为默认设置,我们禁用了“选项”框中的所有控件。这是因为在小部件开始时,没有数据可供采样。但这也意味着,在处理输入令牌时,我们应该注意启用和禁用。我们小部件的数据处理和令牌发送部分现在是
@Inputs.data
def set_data(self, dataset):
if dataset is not None:
self.dataset = dataset
self.infoa.setText('%d instances in input dataset' % len(dataset))
self.optionsBox.setDisabled(False)
self.selection()
else:
self.dataset = None
self.sample = None
self.optionsBox.setDisabled(False)
self.infoa.setText('No data on input yet, waiting to get something.')
self.infob.setText('')
self.commit()
def selection(self):
if self.dataset is None:
return
n_selected = int(numpy.ceil(len(self.dataset) * self.proportion / 100.))
indices = numpy.random.permutation(len(self.dataset))
indices = indices[:n_selected]
self.sample = self.dataset[indices]
self.infob.setText('%d sampled instances' % len(self.sample))
def commit(self):
self.Outputs.sample.send(self.sample)
def checkCommit(self):
if self.commitOnChange:
self.commit()
现在您还可以检查这个小部件的完整代码。为了将其与我们在上一节中开发的小部件区分开来,我们为其设计了一个特殊的图标。如果您希望在Orange Canvas中测试此小部件,请将其代码放在我们为上一个小部件创建的orangedemo目录中,并使用带有File和Data Table小部件的结构进行尝试。
表现良好的小部件会记住它们的设置——复选框和单选按钮的状态、行编辑中的文本、组合框中的选择等等。
持续的默认值 - Persisting defaults
当一个小部件被删除时,它的设置会被存储起来,用作该小部件未来实例的默认设置。
更新后的默认值存储在用户的配置文件中。它的位置取决于操作系统:
(%APPDATA%Orange<version>widgets on windows, ~/Library/ApplicationSupport/orange/<version>/widgets on macOS, ~/.local/share/Orange/<version>/widgets on linux)
Dreamstar: 实际上,配置文件以
.pickle
的文件形式进行储存,如Orange.widgets.data.owfile.OWFile.pickle
。pickle
文件是Python中用于序列化对象的标准文件格式,可以将Python对象转换为字节流并保存到文件中,以便稍后重新加载和使用。使用Python的pickle
模块可以保存数据为.pickle
文件。
可以通过从该文件夹中删除文件、从命令行运行Orange(–clear-widget-settings 小部件设置选项)或通过Options/Reset widget settings菜单操作来恢复原始默认值。
纯净模式设置 - Schema-only settings
某些设置具有不应更改的默认值。例如,当使用Paint Data小部件时,绘制的点应保存在工作流中,但新的小部件应始终以空白页开头——修改后的值不应被记住。
这可以通过使用schema_only标志声明设置来实现。这样的设置与工作流一起保存,但其默认值永远不会更改。
上下文相关设置 - Context dependent settings
上下文相关设置取决于小部件输入。例如,scatter plot小部件包含指定x轴和y轴属性的设置,以及定义图形中示例的颜色、形状和大小的设置。
更复杂的情况是用于数据选择的小部件,使用该小部件可以基于某些属性的值来选择示例。在应用保存的设置之前,这些小部件需要检查它们是否符合实际数据集的域。为了真正有用,上下文相关设置需要为所使用的每个特定数据集保存一个设置配置。也就是说,当给定特定的数据集时,它必须选择适用的、与当前使用的最佳数据集匹配的保存设置。
上下文处理程序负责保存、加载和匹配上下文。目前,只实现了两类上下文处理程序。第一个是抽象ContextHandler
,第二个是DomainContextHandler
(其中上下文由数据集域定义,并且设置包含属性名称) 。后者应该能满足您的大部分需求,而对于更复杂的小部件,您需要从中派生新的类。甚至在某些情况下,上下文不是由域定义的,在这种情况下,ContextHandler将用作新处理程序的基础。
上下文需要声明、打开和关闭。打开和关闭通常发生在处理数据信号的函数中(以相反的顺序)。这就是它在散点图中的样子(为了清晰起见,代码有所简化)。
@Input.data
def set_data(self, data):
self.closeContext()
self.data = data
self.graph.setData(data)
self.initAttrValues()
if data is not None:
self.openContext(data.domain)
self.updateGraph()
self.sendSelections()
一般来说,函数应该是这样的:
- 进行所需的任何清理,但不清除任何需要保存的设置。散点图(Scatter plot)不需要。
- 调用
self.closeContext()
;这样可以确保记住所有上下文相关的设置(例如,列表框中的属性名称)。 - 初始化小部件状态并将控件设置为一些默认值,就好像没有上下文检索机制一样。散点图(Scatter plot)通过调用
self.initAttrValues()
来实现这一点,该函数将前两个属性分配给x和y轴,将class属性分配给颜色。在这个阶段,不应该调用任何依赖于设置的函数,例如绘制图形。 - 调用
self.openContext(data.domain)
(稍后将详细介绍参数)。这将搜索合适的上下文,并在有新值的情况下为控件分配新值。如果没有可使用的已保存上下文,则会创建一个新上下文,并使用上一节点指定的默认值填充该上下文。 - 最后,根据检索到的控件调整小部件。散点图(Scatter plot)现在通过调用
self.updateGraph()
来绘制图形。
当打开上下文时,我们提供上下文所依赖的参数。对于散点图使用的**DomainContextHandler
,我们可以给它一个Orange.data.Domain
**。保存的上下文是否可以重用取决于域中是否存在属性。
如果小部件构造得当(也就是说,如果它严格使用orangewidget.gui
控件而不是Qt控件),则不需要其他管理来切换上下文。
除了声明上下文设置之外,也就是说。散点图(Scatter plot)在其类定义中有这一点
settingsHandler = DomainContextHandler()
attr_x = ContextSetting("")
attr_y = ContextSetting("")
auto_send_selection = Setting(True)
toolbar_selection = Setting(0)
color_settings = Setting(None)
selected_schema_index = Setting(0)
settingsHandler = DomainContextHandler()
声明散点图使用DomainContextHandler
。attr_x
和attr_y
被声明为ContextSetting
。
迁移 - Migrations
在本节教程的开头,我们创建了一个小部件,该小部件具有一个名为“比例”的设置,其中包含一个介于0和100之间的值。但是,想象一下,由于某种原因,我们不再对该值感到满意,并决定该设置应保持0到1之间的值。
我们可以更新了设置的默认值,修改了相应的代码,就完成了。也就是说,直到已经使用旧版本小部件的人打开新版本,小部件才会崩溃。请记住,当打开小部件时,它的设置与上次使用的设置相同。
在调用__init__函数之前,将设置替换为已保存,我们能做些什么吗?有!迁移(Migrations)的方式可以解决。
小部件有一个特殊的属性,叫做settings_version。所有小部件都以settings_version为1开头。当对小部件的设置进行了不兼容的更改时,应增加其settings_version。但自行增加版本并不能解决我们的问题。虽然小部件现在知道它使用了不同的设置,但旧的设置仍然是坏的,需要更新才能与新的小部件一起使用。这可以通过重新实现小部件的方法migrate_settings(用于普通设置)和migrate_context(用于上下文设置)来实现。这两个方法都是使用旧对象及其存储的设置版本调用的。
如果我们在进行上述更改时将settings_version从1更改为2,那么我们的migrate_settings方法将如下所示:
def migrate_settings(settings, version):
if version < 2:
if "proportion" in settings:
settings["proportion"] = settings["proportion"] / 100
您的迁移规则可能很简单,也可能很复杂,但尽量避免简单地忘记值,因为设置也用于保存的工作流中。想象一下,打开一年前用新版Orange设计的复杂工作流程,发现所有设置都恢复为默认设置。不好玩!
警告:
如果以向后不兼容的方式更改现有设置的格式,则还需要更改该设置的名称(change the name of that setting)。否则,旧版本的Orange将无法加载具有新设置格式的工作流。
有两个辅助函数可以使用。Orange.widget.settings.rename_settings(settings,old_name,new_name)
对设置(settings)执行重命名的操作,这些设置可以是字典,也可以是上下文,因此可以从migrate_settings
或migrate_context
调用。
另一个常见操作是将小部件从存储变量名(作为str)升级为存储变量(类的实例-从Variable派生)。在一个典型的场景中,当组合框升级为使用模型时,就会发生这种情况。函数Orange.widget.settings.migrate_str_to_variable(settings,names=None)
对名称中列出的设置进行必要的更改。名称可以是设置名称的列表、单个字符串或“无”。在后一种情况下,所有可能引用变量的设置(即由字符串和int组成的两个元素元组)都会被迁移。
如果由于小部件中的某些更改,导致某些上下文设置变得不适用的情况,该如何操作?例如,一个用于接受任何类型变量的小部件被修改以至于需要一个数值型变量(numeric variable)?具有分类变量的上下文将匹配并被重用…并使小部件崩溃。在这些(极少数)情况下,migrate_context
必须引发异常Orange.widget.settings.IncompatibleContext
和上下文将被删除。
因此,请花一些时间编写迁移,并且在进行中断更改时不要忘记更改settings_version。
通道与令牌 - Channels and Tokens
前面的数据采样器小部件在通道方面是简单且线性的:其设计是接收来自一个小部件的令牌(token),并向另一个小部件发送令牌(token)。就像下面示例中的模式一样:
还有很多与通道和令牌相关的管理,我们将在本节进行概述并制作更复杂的小部件。
多输入通道 - Multi-Input Channels
本质上,关于“多输入”通道的基本思想是,它们可以用来将它们与几个输出通道连接起来。也就是说,如果一个小部件支持这样的通道,那么几个小部件可以同时向该小部件提供输入。
假设,我们想要构建一个小部件,该部件使用数据集,并在上面测试各种预测建模技术。小部件必须有一个输入数据的通道,从之前的教程中,我们知道了如何处理这一问题。但与此不同的是,我们希望将任意多个定义学习者的 widget 连接到我们的测试 widget 上。就像在下面的模式中,使用了三个不同的学习器:
在此,我们将介绍如何定义学习曲线 小部件(widget)的通道,以及如何管理其输入的令牌(token)。在此之前,我们先简单介绍一下:学习曲线(learning curve)可以用来测试某些机器学习算法,以了解其性能如何取决于训练集的大小。为此,我们可以抽取一个较小的数据子集,训练学习分类器,然后在剩余的数据集上进行测试。为了做到这一点(by Salzberg, 1997),我们要进行 k 折交叉验证,但只使用一部分子数据用于训练。输出小部件应该是这样的:
现在回到通道和令牌。我们的小部件的输入和输出通道通过以下方式定义:
class Inputs:
data = Input("Data", Table)
learner = MultiInput("Learner", Learner)
注意,一切都与之前教程中的小部件几乎相同,唯一的区别是我们在输入声明中使用了MultiInput
,它表示该输入可以连接到多个小部件的输出。
处理多个输入信号必须使用set/insert/remove的三元修饰符。
insert函数用于处理整数索引和输入对象实例,并且必须在索引指定的位置插入对象。
@Inputs.learner.insert
def insert_learner(self, index, learner):
"""Insert a learner at index"""
self.learners.insert(index, LearnerData(learner, None, None))
set函数用于指定索引的输入进行更新。
@Inputs.learner
def set_learner(self, index: int, learner):
"""Set the input learner at index"""
# update/replace a learner on a previously connected link
item = self.learners[index]
item.learner = learner
item.results = None
item.curve = None
最后,remove函数用于删除指定索引出的输入。
@Inputs.learner.remove
def remove_learner(self, index):
""""Remove a learner at index"""
# remove a learner and corresponding results
del self.learners[index]
我们将学习者(从数据中学习的对象)和相关联的计算结果存储在LearnerData对象列表中。
class LearnerData:
def __init__(
self,
learner: Learner,
results: Optional[Results] = None,
curve: Optional[Sequence[float]] = None,
) -> None:
self.learner = learner
self.results = results
self.curve = curve
注意,在这个小工具中,评估(k-fold 交叉验证)只进行一次,一旦给定学习器、数据集和评估参数,然后根据评估过程中获得的类概率估计值得出分数。这基本上意味着,从一个评分函数切换到另一个评分函数(并在表格中显示结果)只需要一秒钟的时间。要查看小工具的其余部分,请查看其代码。
使用多个输出通道 - Using Several Output Channels
这里并没有什么新的内容,只是我们需要一个具有多个相同类型输出通道的小部件,以便在下一步中说明默认通道的概念。为此,我们将对前面教程中定义的采样小部件进行修改,使其将采样数据发送到一个通道,并将其他数据发送到另一个通道。该小部件的相应通道定义为:
class Outputs:
sample = Output("Sampled Data", Orange.data.Table)
other = Output("Other Data", Orange.data.Table)
我们在数据采样器小部件(data sampler widget
)的第三版中使用了这个功能,唯一的改动在selection()
和commit()
函数中。
def selection(self):
if self.dataset is None:
return
n_selected = int(numpy.ceil(len(self.dataset) * self.proportion / 100.))
indices = numpy.random.permutation(len(self.dataset))
indices_sample = indices[:n_selected]
indices_other = indices[n_selected:]
self.sample = self.dataset[indices_sample]
self.otherdata = self.dataset[indices_other]
def commit(self):
self.Outputs.sample.send(self.sample)
self.Outputs.sample.send(self.otherdata)
如果一个具有多个相同类型通道的小部件连接到一个接受此类令牌(token)的小部件,Orange Canvas会打开一个窗口,要求用户确认要连接哪些通道。因此,如果我们将*Data Sampler ©*小部件连接到下面模式中的数据表(Data Table)小部件:
我们将获得以下窗口,询问用户有关连接哪些频道的信息:
默认通道(使用相同类型的输入通道时) - Default Channels (When Using Input Channels of the Same Type)
现在,假设我们想扩展我们的学习曲线(learning curve)小部件,使其学习方式与过去相同,但只要定义了这样的数据集,就可以(始终)在相同的外部数据集上测试学习器(learners)。也就是说,除了训练数据集,我们还需要另一个相同类型但用于训练数据集的通道。然而,请注意,大多数情况下,我们只会提供训练数据集,因此我们不想(在Orange Canvas中)被连接到哪个通道的对话框所困扰,因为训练数据集通道将是默认通道。
在申请相同类型的输入通道时,默认通道在通道规格列表中会有一个特殊标记。因此,对于我们的新学习曲线(learning curve
)部件,通道规格为
class Inputs:
data = Input("Data", Table, default=True)
test_data = Input("Test Data", Table)
learner = MultiInput("Learner", Learner)
也就是说,Train Data(训练数据)通道是一个单令牌通道,这是一个默认通道(第三个参数default=True
)。请注意,这些标志可以一起添加(或OR-d),因此Default+Multiple
是一个有效的标志。要测试这是如何工作的,请将文件(file)小部件连接到学习曲线(learning curve)小部件,然后什么都不会发生:
也就是说,由于选择了默认的 “训练数据”,因此不会打开查询连接到哪个通道的窗口。
显式通道 - Explicit Channels
有时,当一个 widget 有多个不同类型的输出时,其中一些输出不应受自动默认连接选择的限制。Orange 的逻辑回归(Logistic Regression)小部件就是一个例子,该小部件会输出一个补充的 "系数(Coefficients)"数据表。此类输出可以标记为 "明确(Explicit)"标志,以确保它们不会被选为默认连接。
响应式GUI - Responsive GUI
现在我们要做的是让小部件具有响应能力,这是最困难的部分。我们需要将把学习评估(learner evaluations)卸载分离到一个单独的线程中。
首先阅读Qt中的线程基础知识,特别是线程和qobject的主题,以及它们如何与Qt的事件循环交互。
我们还必须特别注意,当用户更改算法参数或从画布中移除小部件时,我们可以取消/中断任务(线程)。为此,我们使用了一种称为协作取消(cooperative cancellation)的策略,即我们“要求”挂起的任务停止执行(在GUI线程中),然后在工作线程中定期检查(在已知的预定点上)是否应该继续,如果不提前返回(在我们的情况下,通过引发异常)。
设置 - Setting up
我们使用orangewidget.utils.concurrent.ThreadExecutor
来用于线程的分配/管理(但实际上可以很容易地用stdlib的concurrent.futures.ThreadPoolExecutor
代替它)。
import concurrent.futures
from orangewidget.utils.concurrent import (
FutureWatcher, methodinvoke
)
我们将重新组织我们的代码,使学习者评估(learner evaluation)成为一项明确的任务,因为我们需要跟踪其进度和状态。为此,我们定义了一个Task类。
class Task:
"""
A class that will hold the state for an learner evaluation.
"""
#: A concurrent.futures.Future with our (eventual) results.
#: The OWLearningCurveC class must fill this field
future = ... # type: concurrent.futures.Future
#: FutureWatcher. Likewise this will be filled by OWLearningCurveC
watcher = ... # type: FutureWatcher
#: True if this evaluation has been cancelled. The OWLearningCurveC
#: will setup the task execution environment in such a way that this
#: field will be checked periodically in the worker thread and cancel
#: the computation if so required. In a sense this is the only
#: communication channel in the direction from the OWLearningCurve to the
#: worker thread
cancelled = False # type: bool
def cancel(self):
"""
Cancel the task.
Set the `cancelled` field to True and block until the future is done.
"""
# set cancelled state
self.cancelled = True
# cancel the future. Note this succeeds only if the execution has
# not yet started (see `concurrent.futures.Future.cancel`) ..
self.future.cancel()
# ... and wait until computation finishes
concurrent.futures.wait([self.future])
在小部件(widget)的函数__init__
中,我们创建了ThreadExector
的实例,并初始化了_task
字段。
#: The current evaluating task (if any)
self._task = None # type: Optional[Task]
#: An executor we use to submit learner evaluations into a thread pool
self._executor = concurrent.futures.ThreadPoolExecutor()
以上所有的代码片段均来自OWLearningCurveC.py
。
在线程中启动任务 - Starting a task in a thread
在函数handleNewSignals
中,调用_update
方法。
def handleNewSignals(self):
if len(self.learners):
self.infob.setText("%d learners on input." % len(self.learners))
else:
self.infob.setText("No learners.")
self.commitBtn.setEnabled(len(self.learners))
self._update()
最后,_update
函数(来自 OWLearningCurveC.py
)将启动/安排所有更新。
def _update(self):
if self._task is not None:
# First make sure any pending tasks are cancelled.
self.cancel()
assert self._task is None
if self.data is None:
return
# collect all learners for which results have not yet been computed
need_update = [(i, item) for (i, item) in enumerate(self.learners)
if item.results is None]
if not need_update:
self._update_curve_points()
self._update_table()
return
开始时,我们会取消尚未完成的任务。这样做很重要,我们不能让小工具安排任务后就忘了它们。接下来,我们要进行一些检查,如果没有什么要做,就提前返回。
随后,将学习者评估(learner evaluations)设置为偏函数(partial),并捕获必要的参数:
learners = [item.learner for _, item in need_update]
# setup the learner evaluations as partial function capturing
# the necessary arguments.
if self.testdata is None:
learning_curve_func = partial(
learning_curve,
learners, self.data, folds=self.folds,
proportions=self.curvePoints,
)
else:
learning_curve_func = partial(
learning_curve_with_test_data,
learners, self.data, self.testdata, times=self.folds,
proportions=self.curvePoints,
)
设置任务状态以及主线程和辅助线程之间的通信。从GUI到工作线程的唯一状态(state)流是task.cancelled
字段,它是一个简单的触发线,导致learning_curve
的回调参数引发异常,并将任务完成的百分比设置为100。
# setup the task state
self._task = task = Task()
# The learning_curve[_with_test_data] also takes a callback function
# to report the progress. We instrument this callback to both invoke
# the appropriate slots on this widget for reporting the progress
# (in a thread safe manner) and to implement cooperative cancellation.
set_progress = methodinvoke(self, "setProgressValue", (float,))
def callback(finished):
# check if the task has been cancelled and raise an exception
# from within. This 'strategy' can only be used with code that
# properly cleans up after itself in the case of an exception
# (does not leave any global locks, opened file descriptors, ...)
if task.cancelled:
raise KeyboardInterrupt()
set_progress(finished * 100)
# capture the callback in the partial function
learning_curve_func = partial(learning_curve_func, callback=callback)
另请参阅:
progressBarInit(), progressBarSet(), progressBarFinished()
接下来,我们将函数提交到工作线程中运行,并使用 FutureWatcher 实例在任务完成时通知我们(通过_task_finished
槽)。
self.progressBarInit()
# Submit the evaluation function to the executor and fill in the
# task with the resultant Future.
task.future = self._executor.submit(learning_curve_func)
# Setup the FutureWatcher to notify us of completion
task.watcher = FutureWatcher(task.future)
# by using FutureWatcher we ensure `_task_finished` slot will be
# called from the main GUI thread by the Qt's event loop
task.watcher.done.connect(self._task_finished)
为了使上述代码正常工作,需要将setProgressValue定义为pyqtSlot。
@pyqtSlot(float)
def setProgressValue(self, value):
assert self.thread() is QThread.currentThread()
self.progressBarSet(value)
收集结果 - Collecting results
在_task_finished
(来自OWLearningCurveC.py
)中,我们处理已完成的任务(成功或失败),然后更新显示的分数表。
@pyqtSlot(concurrent.futures.Future)
def _task_finished(self, f):
"""
Parameters
----------
f : Future
The future instance holding the result of learner evaluation.
"""
assert self.thread() is QThread.currentThread()
assert self._task is not None
assert self._task.future is f
assert f.done()
self._task = None
self.progressBarFinished()
try:
results = f.result() # type: List[Results]
except Exception as ex:
# Log the exception with a traceback
log = logging.getLogger()
log.exception(__name__, exc_info=True)
self.error("Exception occurred during evaluation: {!r}"
.format(ex))
# clear all results
for item in self.learners:
item.results = None
else:
# split the combined result into per learner/model results ...
results = [list(Results.split_by_model(p_results))
for p_results in results] # type: List[List[Results]]
assert all(len(r.learners) == 1 for r1 in results for r in r1)
assert len(results) == len(self.curvePoints)
learners = [r.learners[0] for r in results[0]]
# map learner back to LearnerData instance
data_by_learner = {item.learner: item for item in self.learners}
# ... and update self.results
for i, learner in enumerate(learners):
item = data_by_learner[learner]
item.results = [p_results[i] for p_results in results]
# update the display
self._update_curve_points()
self._update_table()
停止 - Stopping
同样令人感兴趣的是cancel
方法。请注意,我们还断开了_task_finished
插槽的连接,这样_task_finished
就不会收到过时的结果。
def cancel(self):
"""
Cancel the current task (if any).
"""
if self._task is not None:
self._task.cancel()
assert self._task.future.done()
# disconnect the `_task_finished` slot
self._task.watcher.done.disconnect(self._task_finished)
self._task = None
self.progressBarFinished()
我们还在cancel
函数中使用onDeleteWidget()
,以在小部件从画布中移除时停止。
def onDeleteWidget(self):
self.cancel()
super().onDeleteWidget()
通用工具 - Utilities
进度条 - Progress Bar
耗时超过一瞬间的操作或者函数可以使用进度条来指示其进度,并且在小部件窗口中的标题栏进行显示。
一共有两个对应的机制来实现进度条的功能,即通过类的操作(Progress bar class)和直接手动操作(Direct manipulation of progress bar)。
进度条class - Progress bar class
Class orangewidget.gui.ProgressBar 可以在小部件中初始化,并给予需要迭代的值。
progress = orangewidget.gui.ProgressBar(self, n)
直接手动操作 - Direct manipulation of progress bar
进度条功能可以直接手动对函数进行数值的操作
progressBarInit(self)
progressBarSet(self, p)
progressBarFinished(self)
progressBarInit 用于初始化进度条,progressBarSet 可以设置完成的百分比, 而progressBarFinished 用于关闭进度条.
在代码中,使用这些方法的时候要配合 try-except 或者 try-finally 的代码块来使用,以确保进度条在出现异常情况的时候被移除。
发出警告和错误 - Issuing warning and errors
小工具可以显示信息、警告和错误。这些信息会显示在 widget 窗口的顶部,并在模式中注明。
简单消息 - Simple messages
如果 Widget 一次只发出一条错误、警告和/或信息,可以调用 self.error(text, shown=True)、 self.warning(text, shown=True) 或 self.information(text, shown=True)。可以同时显示多条信息,但每种信息只能有一条:
self.warning("Discrete features are ignored.")
self.error("Fitting failed due to missing data.")
此时,小部件会出现一条警告和一条错误信息。
self.error(“Fitting failed due to weird data.”)
这将取代原本的错误信息,但警告信息仍然存在。
可通过设置空信息(如 self.error())来移除信息。要删除所有信息,请调用 self.clear_messages()。
如果显示的参数被设为 False,则消息将被删除:
self.error("No suitable features", shown=not self.suitable_features)
以这种方式 "不显示 "信息也会删除任何现有信息。
多条消息 - Multiple messages
如果 Widget 需要同时发布多条独立消息,则要在 widget 类中的本地类中声明这些消息,并从相应的 OWBaseWidget 类中派生出特定类型的消息。例如,一个 widget 类可以包含以下类:
class Error(OWBaseWidget.Error):
no_continuous_features = Msg("No continuous features")
class Warning(OWBaseWidget.Warning):
empty_data = Msg("Comtrongling does not work on meta data")
no_scissors_run = Msg("Do not run with scissors")
ignoring_discrete = Msg("Ignoring {n} discrete features: {}")
在 Widget 中,错误消息可通过调用函数来触发,例如
self.Error.no_continuous_features()
self.Warning.no_scissors_run()
对于简单消息,可以直接添加显示的参数:
self.Warning.no_scissors_run(shown=self.scissors_are_available)
如果消息中包含格式化信息,调用时必须包含格式化方法所需的数据:
self.Warning.ignoring_discrete(", ".join(attrs), n=len(attr))
清除信息的方式是:
self.Warning.ignoring_discrete.clear()
与简单模式一样,可以用以下方法删除多条信息:
self.Warning.clear()
self.clear_messages()
两种类型的消息 - 来自消息类的消息和由 self.error 等触发的消息–可以共存。但要注意的是,删除某些类型的所有消息(例如 self.Error.clear())或所有消息标记(self.clear_message())的方法适用于该类型的所有消息。
**注意:**通过 id 处理多条信息的方式已被弃用,今后将被删除。即self.information(id,text)、self.warning(id,text) 和 self.error(id, text)
I/O摘要 - I/O Summaries
3.19 版新增功能。
小工具可选择使用 set_input_summary()
和 set_output_summary()
方法,通过 info
命名空间总结其输入/输出。
self.info.set_input_summary("foo")
self.info.set_output_summary("bar")
如果给出的是整数,摘要会自动使用公制后缀对其进行格式化,并在工具提示中添加完整的数字。
然后,相关的摘要信息会显示在 widget 窗口下部的状态栏中:
表示无输入/输出的预定义常量分别为 self.info.NoInput
和 self.info.NoOutput
self.info.set_input_summary(self.info.NoInput)
self.info.set_output_summary(self.info.NoOutput)
摘要还可以自定义来包含更详细的信息,在工具提示或弹出窗口中显示:
self.info.set_output_summary("2 animals", "• 1 cat\n• 1 dog")
更多内容:
注意:
最初不会显示 I/O 摘要信息。部件作者应在部件的 init 方法中将其初始化(为空状态)。
提示 - Tips
小工具可以提供图形用户界面中不明显或不公开的功能提示
这些信息存储在 widget 的类属性 UserAdviceMessages 中。在首次显示 widget 时,会从该列表中选择一条信息进行显示。如果用户接受(点击 “好的,我知道了”),选择就会被记录下来,该信息就不会再显示;关闭信息也不会将其标记为已看过。按下Shift + F1
键可以再次显示信息。
UserAdviceMessages 包含 Message
的实例。消息包含一段文字和一个 id(也是字符串),还可选择包含一个图标和一个包含更多信息的 URL。
困惑矩阵小部件(confusion matrix widget)中的设置如下:
UserAdviceMessages = [
widget.Message("Clicking on cells or in headers outputs the "
"corresponding data instances",
"click_cell")]