# 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對監聽函數進行修飾:
- 用Python做科學計算
- 軟件包的安裝和介紹
- NumPy-快速處理數據
- SciPy-數值計算庫
- matplotlib-繪制精美的圖表
- Traits-為Python添加類型定義
- TraitsUI-輕松制作用戶界面
- Chaco-交互式圖表
- TVTK-三維可視化數據
- Mayavi-更方便的可視化
- Visual-制作3D演示動畫
- OpenCV-圖像處理和計算機視覺
- Traits使用手冊
- 定義Traits
- Trait事件處理
- 設計自己的Trait編輯器
- Visual使用手冊
- 場景窗口
- 聲音的輸入輸出
- 數字信號系統
- FFT演示程序
- 頻域信號處理
- Ctypes和NumPy
- 自適應濾波器和NLMS模擬
- 單擺和雙擺模擬
- 分形與混沌
- 關于本書的編寫
- 最近更新
- 源程序集
- 三角波的FFT演示
- 在traitsUI中使用的matplotlib控件
- CSV文件數據圖形化工具
- NLMS算法的模擬測試
- 三維標量場觀察器
- 頻譜泄漏和hann窗
- FFT卷積的速度比較
- 二次均衡器設計
- 單擺擺動周期的計算
- 雙擺系統的動畫模擬
- 繪制Mandelbrot集合
- 迭代函數系統的分形
- 繪制L-System的分形圖