%matplotlib inline
import numpy as np
作者 : Didrik Pinte
Traits项目允许你可以向Python项目属性方便的添加验证、初始化、委托、通知和图形化界面。
在这个教程中,我们将研究Traits工具包并且学习如何动态减少你所写的锅炉片代码,进行快速的GUI应用开发,以及理解Enthought工具箱中其他部分的想法。
Traits和Enthought工具箱是基于BSD-style证书的开源项目。
目标受众
要求
教程内容
Enthought工具箱可以构建用于数据分析、2D绘图和3D可视化的精密应用框架。这些强力可重用的组块是在BSD-style证书下发布的。
Enthought工具箱主要的包是:
在这篇教程中,我们将关注Traits。
在整个这篇教程中,我们将使用基于水资源管理简单案例的一个样例。我们将试着建模一个水坝和水库系统。水库和水坝有下列参数:
水库有一个已知的运转情况。一部分是与基于放水量有关的能量产生。估算水力发电机电力生产的简单公式是$P = \rho hrgk$, 其中
年度的电能生产取决于可用的水供给。在一些设施中,水流率在一年中可能差10倍。
运行状态的第二个部分是蓄水量,蓄水量(storage)依赖于控制和非控制参数:
$storage_{t+1} = storage_t + inflows - release - spillage - irrigation$
本教程中使用的数据不是真实的,可能甚至在现实中没有意义。
trait是可以用于常规Python对象属性的类型定义,给出属性的一些额外特性:
类可以自由混合基于trait的属性与通用Python属性,或者选择允许在这个类中只使用固定的或开放的trait属性集。类定义的Trait属性自动继承自由这个类衍生的其他子类。
创建一个traits类的常用方式是通过扩展HasTraits基础类,并且定义类的traits :
from traits.api import HasTraits, Str, Float
class Reservoir(HasTraits):
name = Str
max_storage = Float
对Traits 3.x用户来说
如果使用Traits 3.x, 你需要调整traits包的命名空间:
像这样使用traits类和使用其他Python类一样简单。注意,trait值通过关键词参数传递:
reservoir = Reservoir(name='Lac de Vouglans', max_storage=605)
所有的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。
自定义默认值可以在代码中定义:
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时,它会被懒惰的调用。例如:
def _name_default(self):
""" Complex initialisation of the reservoir name. """
return 'Undefined'
当用户试图设置trait的内容时,每一个trait都会被验证:
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.
从本质上说,所有的traits都提供关于模型自身的文档。创建类的声明方式使它是自解释的:
from traits.api import HasTraits, Str, Float
class Reservoir(HasTraits):
name = Str
max_storage = Float(100)
trait的desc元数据可以用来提供关于trait更多的描述信息:
from traits.api import HasTraits, Str, Float
class Reservoir(HasTraits):
name = Str
max_storage = Float(100, desc='Maximal storage [hm3]')
现在让我们来定义完整的reservoir类:
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
Traits库也关注用户界面,可以弹出一个Reservoir类的默认视图:
reservoir1 = Reservoir()
reservoir1.edit_traits()
TraitsUI简化了创建用户界面的方式。HasTraits类上的每一个trait都有一个默认的编辑器,将管理trait在屏幕上显示的方式 (即Range trait显示为一个滑块等)。
与Traits声明方式来创建类的相同渠道,TraitsUI提供了声明的界面来构建用户界面代码:
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()
可以将trait定义和它的值推送给另一个对象是Traits的有用的功能。
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方法管理事件和触发器函数:
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为属性使用魔法函数的名字:
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介绍:
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: