在阅读jupyter源码时,发现了Traitlets这个库,对于它的功能和用法做了一些研究,结果让人感到惊喜。Traitlets可以帮助开发人员创建拥有更多丰富特性的类,这样的类一方面扩展了类的功能,一方面,也解决了python语言层面上的痛点,这些特性包括:
我们都清楚,python是动态类型语言,在创建变量时,无需指定变量的类型,变量的类型取决于什么样的对象赋值给它。这就造成了一种潜在的隐患,当一个属性变量必须时int类型时,我们却可以将一个字符串赋值给它,为此,更安全的做法时在赋值时进行类型检查,但这样会耗费我们更多的精力来处理这些细枝末节,如果使用Traitlets就可以完美的解决这类问题。
from traitlets.traitlets import HasTraits
from traitlets import Int
class Student(HasTraits):
age = Int()
stu = Student(age='14')
print(stu.age)
在这段代码里,我定义了一个Student类,它有一个类属性age,类型是Int 在创建Student对象时,如果传入的age参数不是int类型,程序就会引发异常
traitlets.traitlets.TraitError: The 'age' trait of a Student instance must be an int, but a value of '14' <class 'str'> was specified.
这是一个非常好的特性,它可以让我们避免将不合适的对象赋值给特定属性变量,而且,很神奇的一点,age看上去是类属性,但你可以像使用示例属性一样去使用它
from traitlets.traitlets import HasTraits
from traitlets import Int
class Student(HasTraits):
age = Int()
stu = Student(age=14)
stu_2 = Student(age=15)
print(stu.age, stu_2.age)
age的确是类属性,但在创建对象时,traitlets帮我们创建了同名的示例属性,所以,我们可以放心使用age属性,而不用担心修改的是类属性。
traitlets 提供了一种非常方便的计算属性默认值的方法
import getpass
from traitlets.traitlets import HasTraits
from traitlets import Int, Unicode, default
class Identity(HasTraits):
username = Unicode()
@default('username')
def _default_username(self):
return getpass.getuser()
identity = Identity()
print(identity.username)
在不同的环境下,username会有不同的默认值,而不是固定的,使用default装饰器,username属性的值由示例方法_default_username来获得, 很关键的一点,_default_username方法只会被执行一次。
你可以使用property实现类似的功能
class Identity():
@property
def username(self):
return getpass.getuser()
identity = Identity()
print(identity.username)
在最终效果上,两段代码没有本质区别,唯一的区别在于如果你使用property,每次访问username时都会执行示例方法username
当属性修改后,可以发出更改事件,这意味着你可以对属性进行监控
from traitlets.traitlets import HasTraits
from traitlets import Int, Unicode, default
class Foo(HasTraits):
bar = Int(20)
baz = Unicode('python')
foo = Foo()
def func(change):
msg = '{name}修改前等于{old}, 修改后等于{new}'.format(name=change['name'],
old=change['old'],
new=change['new'])
print(msg)
foo.observe(func, names=['bar', 'baz'])
foo.bar = 1
foo.baz = 'abc'
使用observe方法可以指定处理属性变更的方法,names参数指定监控哪些属性,如果你创建的类需要对属性变化进行监控,那么使用traitlets将会非常方便。
如果对某个属性有取值范围的限定,或者其他要求,那么可以对这个属性值进行验证
from traitlets.traitlets import HasTraits
from traitlets import Int, validate, TraitError
class Student(HasTraits):
age = Int()
@validate('age')
def _valid_age(self, proposal):
age = proposal['value']
if age < 13 or age > 16:
raise TraitError('学生年龄异常')
return age
stu = Student(age=18)
当赋值18个age时,不满足逻辑验证,会引发TraitError。
定义一个Book类,有inside_price和sale_price两个属性,分别表示内部价和销售价,其中规定,销售价不能比内部叫高出10, 内部价不能低于20,销售价不得大于50, 如果只考虑两个价格各自的验证条件,处理起来很简单,但是考虑到两个价格之间还有关系,使用之前的方法就难以处理
from traitlets.traitlets import HasTraits
from traitlets import Int, validate, TraitError, Float
class Book(HasTraits):
inside_price = Float()
sale_price = Float()
@validate('inside_price')
def _valid_inside_price(self, proposal):
inside_price = proposal['value']
if inside_price < 20:
raise TraitError('内部价过低')
if (self.sale_price - inside_price) > 10:
raise TraitError('销售价比内部价过高')
return inside_price
@validate('sale_price')
def _valid_sale_price(self, proposal):
sale_price = proposal['value']
if sale_price > 50:
raise TraitError('销售价过高')
return sale_price
book = Book()
book.inside_price = 25
book.sale_price = 40
两个价格,都满足各自的限定条件,但是销售价比内部价格高出了15元,是不符合要求的。在对inside_price赋值的时候,sale_price还没有赋值,因此无法检查他们两个之间的关系,如果你想用调整赋值顺序的方法来解决问题,那么也还是治标不治本,因为验证逻辑也可能会变化,最完美的处理方法是等他们都完成赋值操作之后在进行验证
book = Book()
with book.hold_trait_notifications():
book.inside_price = 25
book.sale_price = 40
使用hold_trait_notifications上下文管理器,当上下文管理器退出时才会执行验证逻辑,这样就可以确保两个属性都是有值的,属性赋值顺序不再影响程序的结果。
QQ交流群: 211426309