3.4 Traits:创建交互对话

In [10]:

%matplotlib inline
import numpy as np

作者 : Didrik Pinte

Traits项目允许你可以向Python项目属性方便的添加验证、初始化、委托、通知和图形化界面。

在这个教程中,我们将研究Traits工具包并且学习如何动态减少你所写的锅炉片代码,进行快速的GUI应用开发,以及理解Enthought工具箱中其他部分的想法。

Traits和Enthought工具箱是基于BSD-style证书的开源项目。

目标受众

Python中高级程序员

要求

教程内容

  • 介绍
  • 例子
  • Traits是什么
    • 初始化
    • 验证
    • 文档
    • 可视化: 打开一个对话框
    • 推迟
    • 通知
    • 一些更高级的特征

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()

注意 缓存属性 当访问一个输入没有改变的属性时,[email protected]_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.

In [ ]:

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: