Traits-为 Python 添加类型定义
Python 作为一种动态编程语言,它的变量没有类型,这种灵活性给快速开发带来很多便利,不过它也不是没有缺点。Traits 库的一个很重要的目的就是为了解决这些缺点所带来的问题。
背景
Traits 库最初是为了开发 Chaco(一个 2D 绘图库) 而设计的,绘图库中有很多绘图用的对象,每个对象都有很多例如线型、颜色、字体之类的属性。为了方便用户使用,每个属性可以允许多种形式的值。例如,颜色属性可以是
- 'red'
- 0xff0000
- (255, 0, 0)
也就是说可以用字符串、整数、组元等类型的值表达颜色,这样的需求初看起来用 Python 的无类型变量是一个很好的选择,因为我们可以把各种各样的值赋值给颜色属性。但是颜色属性虽然可以接受多样的值,却不是能接受所有的值,比如"abc"、0.5 等等就不能很好地表示颜色。而且虽然为了方便用户使用,对外的接口可以接受各种各样形式的值,但是在内部必须有一个统一的表达方式来简化程序的实现。
用 Trait 属性可以很好地解决这样的问题:
- 它可以接受能表示颜色的各种类型的值
- 当给它赋值为不能表达颜色的值时,它能够立即捕捉到错误,并且提供一个有用的错误报告,告诉用户它能够接受什么样的值
- 它提供一个内部的标准的颜色表达方式
让我们来看一下使用 traits 属性表示颜色的例子:
from enthought.traits.api import HasTraits, Color
class Circle(HasTraits):
color = Color
这个程序从 enthought.traits.api 中导入我们需要使用的两个对象: HasTraits 和 Color。所有拥有 trait 属性的类都需要从 HasTraits 继承。由于 Python 的多继承特性,我们很容易将现有的类改为支持 trait 属性。Color 是一个 TraitFactory 对象,我们在 Circle 类的定义中用它来声明一个 color 属性。
熟悉 Python 的朋友可能会对这个程序觉得有些奇怪:按照标准的 Python 语法,直接在 class 下定义的属性 color 应该是属于 Circle 类的属性。而我们这里是希望给 Circle 类的实例一个 color 属性,是不是应该在初始化函数__init__中运行 color = Color 呢?答案是否定的,请记住 trait 属性像类的属性一样定义,像实例的属性一样使用,我们不管 HasTraits 是如何实现这一点的,先来看看如何使用 trait 属性:
>>> c = Circle()
>>> Circle.color
Traceback (most recent call last):
AttributeError: type object 'Circle' has no attribute 'color'
>>> c.color
wx.Colour(255, 255, 255, 255)
我们看到 Circle 类没有 color 属性,而它的实例 c 则有一个 color 属性,其缺省值为 wx.Colour(255, 255, 255, 255)。
>>> c.color = "red"
>>> c.color
wx.Colour(255, 0, 0, 255)
>>> c.color = 0x00ff00
>>> c.color
wx.Colour(0, 255, 0, 255)
>>> c.color = (0, 255, 255)
>>> c.color
wx.Colour(0, 255, 255, 255)
>>> c.color = 0.5
Traceback (most recent call last):
File "c:\python25\lib\site-packages\Traits-3.1.0-py2.5-win32.egg\enthought\
traits\trait_handlers.py", line 175, in error value )
TraitError: The 'color' trait of a Circle instance must be a string of the form
(r,g,b) or (r,g,b,a) where r, g, b, and a are integers from 0 to 255, a wx.Colour
instance, an integer which in hex is of the form 0xRRGGBB, where RR is red, GG is
green, and BB is blue or 'aquamarine' or 'black' or 'blue violet' or 'blue' or
'brown' or 'cadet blue' or 'coral' or 'cornflower blue' or 'cyan' or ...此处略去 N
多英文颜色名... or 'yellow', but a value of 0.5 <type 'float'> was specified.
c.color 支持"red"、0x00ff00 和(0, 255, 255) 等值。但它不支持 0.5 这样的浮点数,于是一个很详细的出错信息告诉我们它所有能支持的值。
在开始下一节之前,最后来看一个很酷的东西:
>>> c.configure_traits()
True
>>> c.color
wx.Colour(64, 34, 117, 255)
执行 c.configure_traits() 之后,出现如下的对话框以供我们修改颜色属性,任意选择一个颜色、按 OK 按钮,看到 configure_traits 函数返回 True,而 c.color 已经变为我们所选择的颜色了。注意你需要在 iPython -wthread 或者 spyder 下运行此函数,否则会出现对话框不响应的问题。
自动生成的修改颜色 Trait 属性的对话框
Traits 是什么
trait 为 Python 对象的属性增加了类型定义的功能,此外还提供了如下的额外功能:
- 初始化:每个 trait 属性都定义有自己的缺省值,这个缺省值用来初始化属性
- 验证:基于 trait 的属性都有明确的类型定义,只有满足定义的值才能赋值给属性
- 委托:trait 属性的值可以委托给其他对象的属性
- 监听:trait 属性的值的改变可以触发指定的函数的运行
- 可视化:拥有 trait 属性的对象可以很方便地提供一个用户界面交互式地改变 trait 属性的值
下面这个简单的例子展示了 trait 所提供的这五项能力:
from enthought.traits.api import Delegate, HasTraits, Instance, Int, Str
class Parent ( HasTraits ):
# 初始化: last_name 被初始化为'Zhang'
last_name = Str( 'Zhang' )
class Child ( HasTraits ):
age = Int
# 验证: father 属性的值必须是 Parent 类的实例
father = Instance( Parent )
# 委托: Child 的实例的 last_name 属性委托给其 father 属性的 last_name
last_name = Delegate( 'father' )
# 监听: 当 age 属性的值被修改时,下面的函数将被运行
def _age_changed ( self, old, new ):
print 'Age changed from %s to %s ' % ( old, new )
下面用这两个类创建立两个实例:
>>> p = Parent()
>>> c = Child()
由于没有设置 c 的 father 属性,因此无法获得它的 last_name 属性:
>>> c.last_name
Traceback (most recent call last):
AttributeError: 'NoneType' object has no attribute 'last_name'
设置 father 属性之后,我们就可以得到 c 的 last_name 了:
>>> c.father = p
>>> c.last_name
'Zhang'
设置 c 的 age 属性将触发_age_changed 方法的执行:
>>> c.age = 4
Age changed from 0 to 4
调用 configure_traits:
>>> c.configure_traits()
True
弹出一个如下的对话框,用户可以通过它修改 c 的 trait 属性,
为 Child 类自动生成的属性修改对话框
可以看到属性按照其英文名排序,垂直排为一列。由于 father 属性是 Parent 类的实例,所以它给我们一个按钮,点此按钮出现下面的设置 father 对象的 tratis 属性的对话框
点击 Child 对话框中的 Father 按钮之后,弹出编辑 father 属性的对话框
在上面这个对话框中修改 father 的 Last name,可以看到 child 的 Last name 属性也随之发生变化。
我们可以调用 print_traits 方法输出所有的 trait 属性与其值:
>>> c.print_traits()
age: 4
father: <__main__.Parent object at 0x13B49120>
last_name: u'Zhang'
调用 get 方法获得一个描述对象所有 trait 属性的 dict:
>>> c.get()
{'age': 4, 'last_name': u'Zhang', 'father': <__main__.Parent object at 0x13B49120>}
此外还可以调用 set 方法设置 trait 属性的值,set 方法可以同时配置多个 trait 的属性:
>>> c.set(age = 6)
Age changed from 4 to 6
<__main__.Child object at 0x13B494B0>
动态添加 Trait 属性
前面介绍的方法都是在类的定义中声明 Trait 属性,在类的实例中使用 Trait 属性。由于 Python 是动态语言,因此 Traits 库也提供了为某个特定的实例添加 Trait 属性的方法。
下面的例子,直接产生 HasTraits 类的一个实例 a, 然后调用其 add_trait 方法动态地为 a 添加一个名为 x 的 Trait 属性,其类型为 Float,初始值为 3.0。
>>> from enthought.traits.api import *
>>> a = HasTraits()
>>> a.add_trait("x", Float(3.0))
>>> a.x
3.0
接下来再创建一个 HasTraits 类的实例 b,用 add_trait 方法为 b 添加一个属性 a,指定其类型为 HasTraits 类的实例。然后把实例 a 赋值给实例 b 的属性 a:b.a。
>>> b = HasTraits()
>>> b.add_trait("a", Instance(HasTraits))
>>> b.a = a
然后为实例 b 添加一个类型为 Delegate(代理) 的属性 y,它是 b 的属性 a 所表示的实例的属性 x 的代理,即 b.y 是 b.a.x 的代理。注意我们在用 Delegate 声明代理时,第一个参数 b 的一个属性名"a",第二个参数是是此属性的属性名"x",modify=True 表示可以通过 b.y 修改 b.a.x 的值。我们看到当将 b.y 的值改为 10 的时候,a.x 的值也同时改变了。
>>> b.add_trait("y", Delegate("a", "x", modify=True))
>>> b.y
3.0
>>> b.y = 10
>>> a.x
10.0
Property 属性
标准的 Python 提供了 Property 功能,Property 看起来像对象的一个成员变量,但是在获取它的值或者给它赋值的时候实际上是调用了相应的函数。Traits 也提供了类似的功能。让我们先来看一个例子:
# -*- coding: utf-8 -*-
# filename: traits_property.py
from enthought.traits.api import HasTraits, Float, Property, cached_property
class Rectangle(HasTraits):
width = Float(1.0)
height = Float(2.0)
#area 是一个属性,当 width,height 的值变化时,它对应的_get_area 函数将被调用
area = Property(depends_on=['width', 'height'])
# 通过 cached_property decorator 缓存_get_area 函数的输出
@cached_property
def _get_area(self):
"""
area 的 get 函数,注意此函数名和对应的 Proerty 名的关系
"""
print 'recalculating'
return self.width * self.height
在 Rectangle 类定义中,使用 Property() 定义了一个 area 属性。Traits 所提供的 Property 和标准 Python 的有所不同,Traits 中根据属性名直接决定了它的访问函数,当用户读取 area 值时,将得到_get_area 函数的返回值;而设置 area 的值时,_set_area 函数将被调用。此外,通过关键字参数 depends_on,指定当 width 和 height 属性变化时自动计算 area 属性。
在_get_area 函数用 @cached_property 进行修饰,使得_get_area 函数的返回值将被缓存,除非 width 和 height 的值发生变化,否则将一直使用缓存的值。下面我们来看看 Rectangle 的用法。在 traits_property.py 的文件夹下,启动 IPython -wthread:
>>> run traits_property.py
>>> r = Rectangle()
>>> r.area # <-- 第一次取得 area,需要进行运算
recalculating
2.0
>>> r.width = 10
>>> r.area # <--修改 width 之后,取得 area,需要进行计算
recalculating
20.0
>>> r.area # <--width 和 height 都没有发生变化,因此直接返回缓存值,没有重新计算
20.0
我们看到通过 depends_on 和 @cached_property,系统可以跟踪 area 属性的状态,判断是否需要调用_get_area 函数重新计算 area 的值。注意在运行 r.width=10 时,并没有立即运行_get_area 函数,这是因为系统知道没有任何物体在监听 r.area 属性,因此它只是保存一个需要重新计算的标志。等到真正需要获取 area 的值时,再调用_get_area 函数。
如果我们调用 r.edit_traits(),就会看到 depends_on 的强大功能了。为了更加有趣一些,这里连续调用两次 edit_traits,弹出两个编辑界面:
>>> r.edit_traits()
<enthought.traits.ui.ui.UI object at 0x02FCD420>
>>> r.edit_traits()
<enthought.traits.ui.ui.UI object at 0x02FD68A0>
修改两个对话框中的任意个 Height 或者 Width 属性都会重新计算 Area,并同时更新对话框显示
然后修改任何一个界面中的 width 或者 height 属性,你可以注意到在输入数值的同时,两个界面中的 Area,Height 和 Width 等各个文本框同时更新,每次键盘按键都会调用_get_area 函数。此时在 IPython 窗口修改 width 的值的话,也会调用_get_area 函数:
>>> r.width = 25
recalculating
当打开界面之后,界面对象开始监听对象 r 的各个属性,因此当我们修改 r.width 之后,系统设置 r.area 的标志为需要重新计算,然后发现 r.area 的值有对象在监听,因此直接调用_get_area 函数更新其值,并且通知所有的监听对象,因此界面就一齐更新了。
让我们来看看在 traits 的内部,是如何处理属性值的改变引起界面变化的:
# -*- coding: utf-8 -*-
# filename: traits_listener.py
from enthought.traits.api import *
class Child ( HasTraits ):
name = Str
age = Int
doing = Str
def __str__(self):
return "%s<%x>" % (self.name, id(self))
# 通知: 当 age 属性的值被修改时,下面的函数将被运行
def _age_changed ( self, old, new ):
print "%s.age changed: form %s to %s" % (self, old, new)
def _anytrait_changed(self, name, old, new):
print "anytrait changed: %s.%s from %s to %s" % (self, name, old, new)
def log_trait_changed(obj, name, old, new):
print "log: %s.%s changed from %s to %s" % (obj, name, old, new)
if __name__ == "__main__":
h = Child(name = "HaiYue", age=4)
k = Child(name = "KaiYu", age=1)
h.on_trait_change(log_trait_changed, name="doing")
Child 类有一个 age 属性,当其值发生变化时,其对应的静态监听函数 _age_changed 将被调用,而 _anytrait_changed 则是一个特殊的静态监听函数,HasTraits 对象的任何 trait 属性值的改变都会调用此函数。
log_trait_changed 是一个普通函数。通过 h.on_trait_change 调用动态地将其与 h 的 doing 属性联系起来,即当 h 对象的 doing 属性改变时,log_trait_changed 函数将被调用。
在 IPython 中运行上面的程序:
>>> run traits_listener.py
anytrait changed: <201ba80>.age from 0 to 4
<201ba80>.age changed: form 0 to 4
anytrait changed: HaiYue<201ba80>.name from to HaiYue
anytrait changed: <201bae0>.age from 0 to 1
<201bae0>.age changed: form 0 to 1
anytrait changed: KaiYu<201bae0>.name from to KaiYu
然后分别改变 h 和 k 这两个对象的各个属性:
>>> h.age = 5
anytrait changed: HaiYue<5d87e70>.age from 4 to 5
HaiYue<5d87e70>.age changed: form 4 to 5
>>> h.doing = "sleeping"
anytrait changed: HaiYue<5d87e70>.doing from to sleeping
log: HaiYue<5d87e70>.doing changed from to sleeping
>>> k.doing = "playing"
anytrait changed: KaiYu<5d874e0>.doing from to playing
Trait 属性的监听函数的调用顺序
静态监听函数的参数有如下几种形式:
- _age_changed(self)
- _age_changed(self, new)
- _age_changed(self, old, new)
- _age_changed(self, name, old, new)
而动态监听函数的参数有如下几种:
- observer()
- ovserver(new)
- ovserver(name, new)
- ovserver(obj, name, new)
- ovserver(obj, name, old, new)
其中 obj 表示属性发生变化的对象,name 为发生改变的属性名,old 为改变前的值,new 为现在值。
动态监听函数不但可是普通函数,还可以是某个对象的方法。
当多个 trait 属性都需要同一个静态监听函数时,用固定函数名就比较麻烦了:你需要写多个_xxx_changed 函数,其中再调用某个函数进行同样的处理。Trait 库提供的解决方案是:用 @on_trait_changed 对监听函数进行修饰:
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论