《用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对监听函数进行修饰。

猜你喜欢

转载自blog.csdn.net/mashaokang1314/article/details/82945340
今日推荐