1. 程式人生 > >《用Python做科學計算》——Traits為Python新增型別定義

《用Python做科學計算》——Traits為Python新增型別定義

Python作為一種動態程式語言,它沒有變數型別,這種靈活性給快速開發帶來了很多便利,不過它也有缺點。Traits庫的一個很重要的目的就是為了解決這些缺點所帶來的問題。 對Traits作用的理解 當函式,類或者一些封裝的通用演算法中的某些部分會因為資料型別不同而導致處理或邏輯不同(而我們又不希望因為資料型別的差異而修改演算法本身的封裝時),traits會是一種很好的解決方案。

背景

Traits庫最初是為了開發Chaco(一個2D繪相簿)而設計的,繪圖中有很多繪圖用的物件,每個物件都有很多例如線型、顏色、字型之類的屬性。為了方便使用,每個屬性可以允許多種形式的值。 例如顏色屬性可以是:

  • ‘red’
  • 0xff0000
  • (255,0,0)

也就是說可以用字串、整數、元組等型別的值表達顏色,這樣的需求初看起來用python的無型別變數是一個很好的選擇,因為我們可以把各種各樣的值賦值給顏色屬性,雖然顏色屬性可以接受多樣的值卻不能接受像"abc"、0.5等這樣的,而且雖然為了方便用胡使用,對外介面可以接受各種各樣形式的值,但是在內部必須有一個統一的表達方式來簡化程式的實現。 用Trait屬性可以很好的解決這些問題:

  • 它可以接受能表示顏色的各種型別的值;
  • 當給它賦值為不能表達顏色的值時,它能立即捕捉到錯誤,並且提供一個有用的錯誤報告,告訴使用者它能夠接受什麼的值;
  • 它提供一個內部的標準的顏色表示式;
#coding:utf-8

from traits.api import HasTraits,Color

# 繼承HasTraits,很容易將現有的類改為支援traits屬性
#Color是一個TraitFactory物件,我們在Circle雷的定義中用它來宣告一個color屬性。
class Circle(HasTraits):

    color=Color
# 因為trait屬性像類的屬性一樣定義,像例項的屬性一樣使用
# 不用初始化為繼承類的屬性

In [3]: c=Circle()

In [4]: Circle.color
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-76706f90976f> in <module>()
----> 1 Circle.color

AttributeError: type object 'Circle' has no attribute 'color'

In [5]: c.color
Out[5]: 'white'

可以看到Circle類沒有color屬性,而它的例項c則有一個color屬性,其初始值為white;

c.color="red"
print(c.color)
# red

c.color=0x00ff00
print(c.color)
# 65280

c.color=(0,255,255,255)
print(c.color)
# (0.0, 1.0, 1.0)

c.color=0.5
print(c.color)

"""
traits.trait_errors.TraitError: The 'color' trait of a Circle instance must
be 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 
'dark green' or 'dark grey' or 'dark olive green' or 'dark orchid' or 
'dark slate blue' or 'dark slate grey' or 'dark turquoise' or 'dim grey'
or 'firebrick' or 'forest green' or 'gold' or 'goldenrod' or 'green yellow'
or 'green' or 'grey' or 'indian red' or 'khaki' or 'light blue' or 'light
grey' or 'light steel' or 'lime green' or 'magenta' or 'maroon' or 'medium 
aquamarine' or 'medium blue' or 'medium forest green' or 'medium goldenrod'
 or 'medium orchid' or 'medium sea green' or 'medium slate blue' or 'medium 
 spring green' or 'medium turquoise' or 'medium violet red' or 'midnight blue' 
 or 'navy' or 'orange red' or 'orange' or 'orchid' or 'pale green' or 'pink' or 
'plum' or 'purple' or 'red' or 'salmon' or 'sea green' or 'sienna' or 'sky blue'
or 'slate blue' or 'spring green' or 'steel blue' or 'tan' or 'thistle' or 'turquoise'
or 'violet red' or 'violet' or 'wheat' or 'white' or 'yellow green' or 'yellow', but a value 
of 0.5 <class 'float'> was specified.

"""

c.color支援“red”、0x00ff00和(0,255,255)等值,但是它不支援0.5這樣的浮點數,於是一個很詳細的出錯資訊告訴我們它所有能支援的值。

執行c.configure_traits()之後,出現如圖對話方塊以供我們修改顏色屬性,任意選擇一個顏色、按OK就可以看到程式碼行中返回了True,顏色已經改變。

注意: 需要在iPython -wthread或者spyder下執行此函式,否則會出現對話方塊不響應的問題。

c.configure_traits()
Out[6]: True

c.color
Out[7]: <PyQt5.QtGui.QColor at 0x7fdc4f5a5208>

在這裡插入圖片描述 在這裡插入圖片描述

Traits是什麼?

trait為Python物件的屬性增加了型別定義的功能,此外還提供瞭如下額外的功能:

  • 初始化:每個trait屬性都定義有自己的預設值,這個預設值以嗯來初始化屬性;
  • 驗證:基於trait的屬性都有明確的型別定義,只有滿足定義的值才能賦值給屬性。
  • 委託:trait屬性的值可以委託給其他物件的屬性;
  • 監聽:trait屬性的值的改變可以觸發指定的函式的執行;
  • 視覺化:擁有trait屬性的物件可以方便地提供一個使用者介面互動式地改變trait屬性的值;
from traits.api import Delegate,HasTraits,Instance,Int,Str

class Parent(HasTraits):

    last_name=Str('Zhang')
#     初始化:last_name被初始化為‘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 ))
if __name__=="__main__":
        # 建立例項
    p=Parent()
    c=Child()
# 由於沒有設定c的father屬性,因此無法獲得
    print(c.last_name)
    # AttributeError: 'Child' object has no attribute 'last_name'
#   設定之後再獲取
    c.father=p
    print(c.last_name)
    # Zhang

設定c的age屬性將觸發_age_chandged方法的執行:

c.age=4
        # Age changed from 0 to 4 

呼叫configure_traits:

c.father=p
c.configure_traits()

在對話方塊中修改c的屬性值; 在這裡插入圖片描述

在對話方塊修改了年齡,可以看到觸發了函式。 點選Father按鈕修改資訊,可以看到c的屬性也發生了改變; 在這裡插入圖片描述 呼叫c.print_traits()方法輸出所有的trait屬性與其值:

c.print_traits()
#age:       4
#father:    <__main__.Parent object at 0x7fe1db222a40>
#last_name: 'Zhang11'

呼叫get方法獲得一個描述物件所有trait屬性的dict:

print(c.get())
    #{'age': 4, 'father': <__main__.Parent object at 0x7f29b93b09e8>, 'last_name': 'Zhang11'}
 

還可以呼叫set方法設定trait屬性的值,set方法可以同時配置多個trait的屬性:

print(c.set(age=6))
        # Age changed from 4 to 6 
	# <__main__.Child object at 0x7fe87a812bf8>

動態新增Trait屬性

前面介紹的方法都是在類的定義中宣告Trait屬性,在類的例項中使用Trait屬性。由於Python是動態語言,因此Traits庫也提供了為某個特定的例項新增Trait屬性的方法。 例項: 直接產生HasTtraits類的一個例項a然後呼叫其add_trait方法動態地為a新增一個名為x的Trait屬性,其型別為Float,初始值為3.0

In [1]: from traits.api import *

In [2]: a=HasTraits()

In [3]: a.add_trait("x",Float(3.0))

In [4]: a.x
Out[4]: 3.0

接下來在建立一個HasTraits類的例項b,用add_trait方法為b新增一個屬性a,指定其型別為HasTraits類的例項。然後把例項a賦值給例項b的屬性a:b.a。

In [5]: b=HasTraits()

In [6]: b.add_trait("a",Instance(HasTraits))

In [7]: b.a=a

然後為例項b新增一個型別為Delegate(代理)的屬性y,它是b的屬性a所表示的例項的屬性x的代理,即b.y是b.a.x的代理。 注意我們在用Delegate宣告代理的時候,第一個引數b的一個屬性名“a”,第二個引數是此屬性的屬性ign“x”,modify=True表示可以通過b.y修改b.a.x的值。我們將b.y的值改為10的時候,a.x的值也改變了。

In [8]: b.add_trait("y",Delegate("a","x",modify=True))

In [9]: b.y
Out[9]: 3.0

In [10]: b.y=10

In [11]: a.x
Out[11]: 10.0

Property屬性

標準的Python提供了Property功能,Property看起來像物件的一個成員變數,但是在獲取它的值或者給它賦值的時候實際上時呼叫了相應的函式。Traits也提供了類似的功能。

from traits.api import HasTraits,Float,Property,cached_property



class Rectnagle(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的值發生變化,否則將一直使用快取的值。

In [1]: run 18_Traits_Property.py

In [2]: r=Rectangle()

In [3]: #第一次取得area,需要呼叫area的計算函式

In [4]: r.area
recalculating
Out[4]: 2.0

In [5]: #修改width之後,使用area屬性,也需要呼叫area計算函式

In [6]: r.width=10

In [7]: r.area
recalculating
Out[7]: 20.0

In [8]: #不改變width和height

In [9]: r.area
Out[9]: 20.0

In [10]: #直接返回之前快取的值,沒有呼叫計算函式

通過depends_on和cached_property,系統可以跟蹤area屬性的狀態,判斷是否呼叫_get_area函式重新計算area的值。 注意,在執行r.width=10時並沒有立即執行_get_area函式,這是因為系統知道沒有任何物體在監聽r.area屬性,因此它只是儲存一個需要重新計算的標誌。等到真正獲取area的值時,再呼叫_get_area函式。

呼叫r.configure_traits()會彈出一個編輯介面,修改數值會發現每次按鍵area的值都會發生改變,所以每次按鍵都會呼叫_get_area函式更新其值,並且通知所有監聽物件。

In [3]: r.configure_traits()

recalculating
Out[3]: True

In [4]: r.width
Out[4]: 1999.0

In [5]: r.height
Out[5]: 6776.0

In [6]: r.area
Out[6]: 13545224.0

在這裡插入圖片描述 內部執行機制

獲得與area屬性對應的Trait,此物件儲存了area屬性運作需要的資訊

In [7]: t=r.trait("area")

In [8]: type(t)
Out[8]: traits.traits.CTrait

In [9]: #一個CTrait物件
In [10]: t
Out[10]: <traits.traits.CTrait at 0x7fbbe4126630>

_notifiers函式返回所有的通知物件,當area屬性改變時,這裡物件將被通知

In [15]: t._notifiers(True)
Out[15]: []


Trait屬性監聽

HasTraits類的所有物件的所有trait屬性都自動支援監聽功能。當某個trait屬性值發生改變時,HasTraits物件胡uitongzhi所有監聽此屬性的函式。 監聽函式分為靜態和動態兩種

#coding:utf-8

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

    #任何trait屬性值改變都會呼叫這個函式
    def _anytrait_changed(self,name,old,new):
        print("anytrait changed:%s.%s from %s to %s" %(self,name,old,new))

# 通過h.on_trait_change呼叫動態地將h的的doing屬性聯絡起來
# 即當h物件的的的doing屬性改變時,呼叫這個函式
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="Harry",age=4)
    k=Child(name="kittle",age=6)
    h.on_trait_change(log_trait_changed,name="doing")

執行這個檔案,可以看到屬性值被改變

In [25]: run traits_listener.py
anytrait changed:Harry<7fbbdc9d6468>.name from  to Harry
anytrait changed:Harry<7fbbdc9d6468>.age from 0 to 4
Harry<7fbbdc9d6468>.age changed:form 0 to 4
anytrait changed:kittle<7fbbdc9d6360>.name from  to kittle
anytrait changed:kittle<7fbbdc9d6360>.age from 0 to 6
kittle<7fbbdc9d6360>.age changed:form 0 to 6

改變屬性值,可以看到函式被呼叫

In [26]: h.age=2
anytrait changed:Harry<7fbbdc9d6468>.age from 4 to 2
Harry<7fbbdc9d6468>.age changed:form 4 to 2

In [27]: h.doing="sleeping"
anytrait changed:Harry<7fbbdc9d6468>.doing from  to sleeping
log:Harry<7fbbdc9d6468>.doing changed from  to sleeping

In [28]: h.doing="playing"
anytrait changed:Harry<7fbbdc9d6468>.doing from sleeping to playing
log:Harry<7fbbdc9d6468>.doing changed from sleeping to playing

監聽函式按照順序被呼叫

靜態監聽函式:_anytrait_changed——>_age_changed——>動態監聽

靜態監聽函式的引數有如下幾種形式: • _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表示屬性發生變化的時候的物件

動態監聽函式不但可以是普通函式,還可以是某個物件的方法。 當多個trait屬性都需要同一個靜態監聽函式時,用固定的函式名就比較麻煩,Trait庫提供瞭解決方案:用@on_trait_changed對監聽函式進行修飾。