Traitlets---帮你创建更有特性的类

在阅读jupyter源码时,发现了Traitlets这个库,对于它的功能和用法做了一些研究,结果让人感到惊喜。Traitlets可以帮助开发人员创建拥有更多丰富特性的类,这样的类一方面扩展了类的功能,一方面,也解决了python语言层面上的痛点,这些特性包括:

  1. 具有类型检查和动态计算的默认值的属性
  2. 属性修改后,特征发出更改事件
  3. 执行一些验证,并允许在分配时强制使用新特征值

1. 类型检查

我们都清楚,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属性,而不用担心修改的是类属性。

2. 动态计算的默认值

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

3. 观察者模式,属性修改后,特征发出更改事件

当属性修改后,可以发出更改事件,这意味着你可以对属性进行监控

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将会非常方便。

4. 自定义验证逻辑

如果对某个属性有取值范围的限定,或者其他要求,那么可以对这个属性值进行验证

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。

5. 处理属性值之间互相影响的情况

定义一个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

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案