3.4 Traits:创建交互对话
Traits 项目允许你可以向 Python 项目属性方便的添加验证、初始化、委托、通知和图形化界面。
在这个教程中,我们将研究Traits工具包并且学习如何动态减少你所写的锅炉片代码,进行快速的GUI应用开发,以及理解Enthought工具箱中其他部分的想法。
Traits和Enthought工具箱是基于BSD-style证书的开源项目。
目标受众:Python 中高级程序员
要求
- wxPython、PyQt或PySide之一
- Numpy和Scipy
- Enthought工具箱
- 所有需要的软件都可以通过安装EPD免费版来获得
3.4.1 介绍
Enthought工具箱可以构建用于数据分析、2D绘图和3D可视化的精密应用框架。这些强力可重用的组块是在BSD-style证书下发布的。
Enthought工具箱主要的包是:
- Traits - 基于组块的方式构建我们的应用。
- Kiva - 2D原生支持基于路径的rendering、affine转化、alpha混合及其它。
- Enable - 基于对象的2D绘图画布。
- Chaco - 绘图工具箱,用于构建复杂的交互2D图像。
- Mayavi -基于VTK的3D科学数据可视化
- Envisage - 应用插件框架,用于构建脚本化可扩展的应用
在这篇教程中,我们将关注 Traits。
3.4.2 例子
在整个这篇教程中,我们将使用基于水资源管理简单案例的一个样例。我们将试着建模一个水坝和水库系统。水库和水坝有下列参数:
- 名称
- 水库的最小和最大容量 [$hm^3$]
- 水坝的高度和宽度[$m$]
- 蓄水面积[$km^2$]
- 水压头[$m$]
- 涡轮的动力[$MW$]
- 最小和最大放水量[$m^3/s$]
- 涡轮的效率
水库有一个已知的运转情况。一部分是与基于放水量有关的能量产生。估算水力发电机电力生产的简单公式是$P = \rho hrgk$, 其中
- P 以瓦特为单位的功率,
- \rho 是水的密度 ($~1000 kg/m^3$),
- h 是水的高度,
- r 是以每秒立方米为单位的流动率,
- g 重力加速度,9.8 $m/s^2$,
- k 是效率系数,范围从0到1。
年度的电能生产取决于可用的水供给。在一些设施中,水流率在一年中可能差10倍。
运行状态的第二个部分是蓄水量,蓄水量(storage)依赖于控制和非控制参数:
$storage_{t+1} = storage_t + inflows - release - spillage - irrigation$
本教程中使用的数据不是真实的,可能甚至在现实中没有意义。
3.4.3 Traits是什么
trait是可以用于常规Python对象属性的类型定义,给出属性的一些额外特性:
- 标准化:
- 初始化
- 验证
- 推迟
- 通知
- 可视化
- 文档
类可以自由混合基于trait的属性与通用Python属性,或者选择允许在这个类中只使用固定的或开放的trait属性集。类定义的Trait属性自动继承自由这个类衍生的其他子类。
创建一个traits类的常用方式是通过扩展HasTraits基础类,并且定义类的traits :
In [1]:
from traits.api import HasTraits, Str, Float class Reservoir(HasTraits): name = Str max_storage = Float
对Traits 3.x用户来说
如果使用Traits 3.x, 你需要调整traits包的命名空间:
- traits.api应该为enthought.traits.api
- traitsui.api应该为enthought.traits.ui.api
像这样使用traits类和使用其他Python类一样简单。注意,trait值通过关键词参数传递:
In [2]:
reservoir = Reservoir(name='Lac de Vouglans', max_storage=605)
3.4.3.1 初始化
所有的traits都有一个默认值来初始化变量。例如,基础python类型有如下的trait等价物:
Trait | Python类型 | 内置默认值 |
---|---|---|
Bool | Boolean | False |
Complex | Complex number | 0+0j |
Float | Floating point number | 0.0 |
Int | Plain integer | 0 |
Long | Long integer | 0L |
Str | String | '' |
Unicode | Unicode | u'' |
存在很多其他预定义的trait类型: Array, Enum, Range, Event, Dict, List, Color, Set, Expression, Code, Callable, Type, Tuple, etc。
自定义默认值可以在代码中定义:
In [3]:
from traits.api import HasTraits, Str, Float class Reservoir(HasTraits): name = Str max_storage = Float(100) reservoir = Reservoir(name='Lac de Vouglans')
复杂初始化
当一个trait需要复杂的初始化时,可以实施_XXX_默认魔法方法。当调用XXX trait时,它会被懒惰的调用。例如:
In [4]:
def _name_default(self): """ Complex initialisation of the reservoir name. """ return 'Undefined'
3.4.3.2 验证
当用户试图设置trait的内容时,每一个trait都会被验证:
In [5]:
reservoir = Reservoir(name='Lac de Vouglans', max_storage=605) reservoir.max_storage = '230'
--------------------------------------------------------------------------- TraitError Traceback (most recent call last) <ipython-input-5-cbed071af0b9> in <module>() 1 reservoir = Reservoir(name='Lac de Vouglans', max_storage=605) 2 ----> 3 reservoir.max_storage = '230' /Library/Python/2.7/site-packages/traits/trait_handlers.pyc in error(self, object, name, value) 170 """ 171 raise TraitError( object, name, self.full_info( object, name, value ), --> 172 value ) 173 174 def full_info ( self, object, name, value ): TraitError: The 'max_storage' trait of a Reservoir instance must be a float, but a value of '230' <type 'str'> was specified.
3.4.3.3 文档
从本质上说,所有的traits都提供关于模型自身的文档。创建类的声明方式使它是自解释的:
In [6]:
from traits.api import HasTraits, Str, Float class Reservoir(HasTraits): name = Str max_storage = Float(100)
trait的desc元数据可以用来提供关于trait更多的描述信息:
In [7]:
from traits.api import HasTraits, Str, Float class Reservoir(HasTraits): name = Str max_storage = Float(100, desc='Maximal storage [hm3]')
现在让我们来定义完整的reservoir类:
In [8]:
from traits.api import HasTraits, Str, Float, Range class Reservoir(HasTraits): name = Str max_storage = Float(1e6, desc='Maximal storage [hm3]') max_release = Float(10, desc='Maximal release [m3/s]') head = Float(10, desc='Hydraulic head [m]') efficiency = Range(0, 1.) def energy_production(self, release): ''' Returns the energy production [Wh] for the given release [m3/s] ''' power = 1000 * 9.81 * self.head * release * self.efficiency return power * 3600 if __name__ == '__main__': reservoir = Reservoir( name = 'Project A', max_storage = 30, max_release = 100.0, head = 60, efficiency = 0.8 ) release = 80 print 'Releasing {} m3/s produces {} kWh'.format( release, reservoir.energy_production(release) )
Releasing 80 m3/s produces 1.3561344e+11 kWh
3.4.3.4 可视化: 打开一个对话框
Traits库也关注用户界面,可以弹出一个Reservoir类的默认视图:
In [ ]:
reservoir1 = Reservoir() reservoir1.edit_traits()
TraitsUI简化了创建用户界面的方式。HasTraits类上的每一个trait都有一个默认的编辑器,将管理trait在屏幕上显示的方式 (即Range trait显示为一个滑块等)。
与Traits声明方式来创建类的相同渠道,TraitsUI提供了声明的界面来构建用户界面代码:
In [ ]:
from traits.api import HasTraits, Str, Float, Range from traitsui.api import View class Reservoir(HasTraits): name = Str max_storage = Float(1e6, desc='Maximal storage [hm3]') max_release = Float(10, desc='Maximal release [m3/s]') head = Float(10, desc='Hydraulic head [m]') efficiency = Range(0, 1.) traits_view = View( 'name', 'max_storage', 'max_release', 'head', 'efficiency', title = 'Reservoir', resizable = True, ) def energy_production(self, release): ''' Returns the energy production [Wh] for the given release [m3/s] ''' power = 1000 * 9.81 * self.head * release * self.efficiency return power * 3600 if __name__ == '__main__': reservoir = Reservoir( name = 'Project A', max_storage = 30, max_release = 100.0, head = 60, efficiency = 0.8 ) reservoir.configure_traits()
3.4.3.5 推迟
可以将trait定义和它的值推送给另一个对象是Traits的有用的功能。
In [ ]:
from traits.api import HasTraits, Instance, DelegatesTo, Float, Range from reservoir import Reservoir class ReservoirState(HasTraits): """Keeps track of the reservoir state given the initial storage. """ reservoir = Instance(Reservoir, ()) min_storage = Float max_storage = DelegatesTo('reservoir') min_release = Float max_release = DelegatesTo('reservoir') # state attributes storage = Range(low='min_storage', high='max_storage') # control attributes inflows = Float(desc='Inflows [hm3]') release = Range(low='min_release', high='max_release') spillage = Float(desc='Spillage [hm3]') def print_state(self): print 'Storage\tRelease\tInflows\tSpillage' str_format = '\t'.join(['{:7.2f}'for i in range(4)]) print str_format.format(self.storage, self.release, self.inflows, self.spillage) print '-' * 79 if __name__ == '__main__': projectA = Reservoir( name = 'Project A', max_storage = 30, max_release = 100.0, hydraulic_head = 60, efficiency = 0.8 ) state = ReservoirState(reservoir=projectA, storage=10) state.release = 90 state.inflows = 0 state.print_state() print 'How do we update the current storage ?'
特殊的trait允许用魔法_xxxx_fired方法管理事件和触发器函数:
In [ ]:
from traits.api import HasTraits, Instance, DelegatesTo, Float, Range, Event from reservoir import Reservoir class ReservoirState(HasTraits): """Keeps track of the reservoir state given the initial storage. For the simplicity of the example, the release is considered in hm3/timestep and not in m3/s. """ reservoir = Instance(Reservoir, ()) min_storage = Float max_storage = DelegatesTo('reservoir') min_release = Float max_release = DelegatesTo('reservoir') # state attributes storage = Range(low='min_storage', high='max_storage') # control attributes inflows = Float(desc='Inflows [hm3]') release = Range(low='min_release', high='max_release') spillage = Float(desc='Spillage [hm3]') update_storage = Event(desc='Updates the storage to the next time step') def _update_storage_fired(self): # update storage state new_storage = self.storage - self.release + self.inflows self.storage = min(new_storage, self.max_storage) overflow = new_storage - self.max_storage self.spillage = max(overflow, 0) def print_state(self): print 'Storage\tRelease\tInflows\tSpillage' str_format = '\t'.join(['{:7.2f}'for i in range(4)]) print str_format.format(self.storage, self.release, self.inflows, self.spillage) print '-' * 79 if __name__ == '__main__': projectA = Reservoir( name = 'Project A', max_storage = 30, max_release = 5.0, hydraulic_head = 60, efficiency = 0.8 ) state = ReservoirState(reservoir=projectA, storage=15) state.release = 5 state.inflows = 0 # release the maximum amount of water during 3 time steps state.update_storage = True state.print_state() state.update_storage = True state.print_state() state.update_storage = True state.print_state()
对象间的依赖可以自动使用traitProperty完成。depends_on属性表示property其他traits的依赖性。当其他traits改变了,property是无效的。此外,Traits为属性使用魔法函数的名字:
- _get_XXX 来获得XXX属性的trait
- _set_XXX 来设置XXX属性的trait
In [ ]:
from traits.api import HasTraits, Instance, DelegatesTo, Float, Range from traits.api import Property from reservoir import Reservoir class ReservoirState(HasTraits): """Keeps track of the reservoir state given the initial storage. For the simplicity of the example, the release is considered in hm3/timestep and not in m3/s. """ reservoir = Instance(Reservoir, ()) max_storage = DelegatesTo('reservoir') min_release = Float max_release = DelegatesTo('reservoir') # state attributes storage = Property(depends_on='inflows, release') # control attributes inflows = Float(desc='Inflows [hm3]') release = Range(low='min_release', high='max_release') spillage = Property( desc='Spillage [hm3]', depends_on=['storage', 'inflows', 'release'] ) ### Private traits. _storage = Float ### Traits property implementation. def _get_storage(self): new_storage = self._storage - self.release + self.inflows return min(new_storage, self.max_storage) def _set_storage(self, storage_value): self._storage = storage_value def _get_spillage(self): new_storage = self._storage - self.release + self.inflows overflow = new_storage - self.max_storage return max(overflow, 0) def print_state(self): print 'Storage\tRelease\tInflows\tSpillage' str_format = '\t'.join(['{:7.2f}'for i in range(4)]) print str_format.format(self.storage, self.release, self.inflows, self.spillage) print '-' * 79 if __name__ == '__main__': projectA = Reservoir( name = 'Project A', max_storage = 30, max_release = 5, hydraulic_head = 60, efficiency = 0.8 ) state = ReservoirState(reservoir=projectA, storage=25) state.release = 4 state.inflows = 0 state.print_state()
注意 缓存属性
当访问一个输入没有改变的属性时,大量计算或长时间运行的计算会是个问题。@cached_property修饰器可以用来缓存这个值,并且只有在失效时才会重新计算一次他们。
让我们用ReservoirState的例子来扩展TraitsUI介绍:
In [ ]:
from traits.api import HasTraits, Instance, DelegatesTo, Float, Range, Property from traitsui.api import View, Item, Group, VGroup from reservoir import Reservoir class ReservoirState(HasTraits): """Keeps track of the reservoir state given the initial storage. For the simplicity of the example, the release is considered in hm3/timestep and not in m3/s. """ reservoir = Instance(Reservoir, ()) name = DelegatesTo('reservoir') max_storage = DelegatesTo('reservoir') max_release = DelegatesTo('reservoir') min_release = Float # state attributes storage = Property(depends_on='inflows, release') # control attributes inflows = Float(desc='Inflows [hm3]') release = Range(low='min_release', high='max_release') spillage = Property( desc='Spillage [hm3]', depends_on=['storage', 'inflows', 'release'] ) ### Traits view traits_view = View( Group( VGroup(Item('name'), Item('storage'), Item('spillage'), label = 'State', style = 'readonly' ), VGroup(Item('inflows'), Item('release'), label='Control'), ) ) ### Private traits. _storage = Float ### Traits property implementation. def _get_storage(self): new_storage = self._storage - self.release + self.inflows return min(new_storage, self.max_storage) def _set_storage(self, storage_value): self._storage = storage_value def _get_spillage(self): new_storage = self._storage - self.release + self.inflows overflow = new_storage - self.max_storage return max(overflow, 0) def print_state(self): print 'Storage\tRelease\tInflows\tSpillage' str_format = '\t'.join(['{:7.2f}'for i in range(4)]) print str_format.format(self.storage, self.release, self.inflows, self.spillage) print '-' * 79 if __name__ == '__main__': projectA = Reservoir( name = 'Project A', max_storage = 30, max_release = 5, hydraulic_head = 60, efficiency = 0.8 ) state = ReservoirState(reservoir=projectA, storage=25) state.release = 4 state.inflows = 0 state.print_state() state.configure_traits()
Some use cases need the delegation mechanism to be broken by the user when setting the value of the trait. The PrototypeFrom trait implements this behaviour.
TraitsUI simplifies the way user interfaces are created. Every trait on a HasTraits class has a default editor that will manage the way the trait is rendered to the screen (e.g. the Range trait is displayed as a slider, etc.). In the very same vein as the Traits declarative way of creating classes, TraitsUI provides a declarative interface to build user interfaces code: