如何在列表上触发Traits静态事件通知?

6 投票
2 回答
1536 浏览
提问于 2025-04-17 07:38

我正在学习一个叫做 traits 的东西,这是在2010年PyCon大会上的一个演示。大约在2小时30分钟的时候,演讲者开始讲解 trait事件通知,这让我们可以在某个 trait 发生变化时,自动调用一个子程序。

我在运行他给的例子,但我做了一些修改……这次实验中,我想看看每当我改变 volumevolume_inputs 时,是否能触发一个静态事件。

# Filename: spinaltap.py
from traits.api import HasTraits, Range, List, Float
import traits

class Amplifier(HasTraits):
    """
    Define an Amplifier (ref -> Spinal Tap) with Enthought's traits.  Use traits
    to enforce values boundaries on the Amplifier's attributes.  Use events to
    notify via the console when the volume trait is changed and when new volume
    traits are added to inputs.
    """

    # Define a volume trait as a Float between 0.0 and 11.0 (inclusive)
    # see self._volume_changed()
    volume = Range(value=5.0, trait=Float, low=0.0, high=11.0)

    # Define an inputs trait as a List() containing volume traits
    volume_inputs = List(volume) # <-- fire a static trait notification
                             # when another volume element is added
                             # see self._volume_inputs_changed()

    def __init__(self, volume=5.0):
        super(Amplifier, self).__init__()
        self.volume = volume
        self.volume_inputs.append(volume)

    def _volume_changed(self, old, new):
        # This is a static event listener for self.volume
        #                                     ^^^^^^^^^^^
        if not (new in self.inputs):
            self.inputs.append(self.volume)
        if new == 11.0:
            print("This one goes to eleven... so far, we have seen", self.inputs)

    def _volume_inputs_changed(self, old, new):
        # This is a static event listener for self.volume_inputs
        #                                     ^^^^^^^^^^^^^^^^^^
        print("Check it out!!")

if __name__=='__main__':
    spinal_tap = Amplifier()
    candidate_volume = 4.0
    spinal_tap.event_fired = False
    print("- INITIAL_VALUE var volume_inputs = {}".format(spinal_tap.volume_inputs))
    print("- APPEND a new volume of 4.0")
    print("    - volume_inputs = {} # BEGIN".format(spinal_tap.volume_inputs))
    print("    - volume_inputs.append({})".format(candidate_volume))
    spinal_tap.volume_inputs.append(candidate_volume)
    print("    - volume_inputs: {} # END".format(spinal_tap.volume_inputs))
    if spinal_tap.event_fired is False:
        print("    - Test FAILED: Traits did not fire _volume_inputs_changed()")
    else:
        print("    - Test PASSED: Traits fired _volume_inputs_changed()")
    try:
        spinal_tap.event_fired = False
        print("- NEGATIVE Test... try to append 12.0.  This should fail; 12.0 is out of bounds")
        print("    - volume_inputs: {} # BEGIN".format(spinal_tap.volume_inputs))
        candidate_volume = 12.0
        print("    - volume_inputs.append({})".format(candidate_volume))
        spinal_tap.volume_inputs.append(candidate_volume)
        print("    - volume_inputs: {} # END".format(spinal_tap.volume_inputs))
        if spinal_tap.event_fired is False:
            print("    - Test FAILED: Traits did not fire _volume_inputs_changed()")
    except  traits.trait_errors.TraitError:
        print("    - TraitError raised --> HERE <--")
        print("    - volume_inputs: {} # END".format(spinal_tap.volume_inputs))
        print("    - Test PASSED: traits correctly raised TraitError instead of appending {}.".format(candidate_volume))

问题是 -> 我从来没有看到过 _volume_inputs_changed() 这个事件。无论我尝试什么例子,都无法让 List 触发事件。

在下面的输出中,没有任何证据表明 _volume_inputs_changed() 曾经被触发过。

[mpenning@Bucksnort ~]$ python spinaltap.py
- INITIAL_VALUE var volume_inputs = [5.0]
- APPEND a new volume of 4.0
    - volume_inputs = [5.0] # BEGIN
    - volume_inputs.append(4.0)
    - volume_inputs: [5.0, 4.0] # END
    - Test FAILED: Traits did not fire _volume_inputs_changed()
- NEGATIVE Test... try to append 12.0.  This should fail; 12.0 is out of bounds
    - volume_inputs: [5.0, 4.0] # BEGIN
    - volume_inputs.append(12.0)
    - TraitError raised --> HERE <--
    - volume_inputs: [5.0, 4.0] # END
    - Test PASSED: traits correctly raised TraitError instead of appending 12.0.
[mpenning@Bucksnort ~]$

使用traits时,List() 应该能够触发一个静态的 List() 事件(比如 _inputs_changed())吗?如果可以的话,我是不是做错了什么?

2 个回答

1

最近我也遇到了这个问题,所以我验证了一下Mike Pennington的回答,使用的是Traits 4.2.1版本。看起来在对列表的操作上,有些区别是存在的。比如说,直接给列表赋值(也就是给它一个新的列表),和对列表中的内容进行修改(比如添加新项或通过索引设置某个项)是不同的。前者使用的名称和列表的特性是一样的(比如inputs),而后者则会在名称后面加上“_items”的后缀。这个例子似乎很好地说明了这一点:

from traits.api import Float, HasTraits, Instance, List

class Part(HasTraits):
    costs = List(Float)

    # called when the actual List trait changes:
    def _costs_changed(self, old, new):
        print("Part::_costs_changed %s -> %s" % (str(old), str(new)))

    # called when the contents of the List trait changes:
    def _costs_items_changed(self, old, new):
        print("Part::_costs_changed %s -> %s" % (str(old), str(new)))

class Widget(HasTraits):
    part = Instance(Part)

    def __init__(self):
        self.part = Part()
        self.part.on_trait_change(self.update_costs, 'costs')
        self.part.on_trait_change(self.update_costs_items, 'costs_items')

    def update_costs(self, name, new):
        print("update_costs: %s = %s" % (name, str(new),))

    def update_costs_items(self, name, new):
        print("update_costs_items: %s = %s" % (name, str(new),))

w = Widget()

w.part.costs = [ 1.0, 2.0, 3.0 ]
# Part::_costs_changed [] -> [1.0, 2.0, 3.0]
# update_costs: costs = [1.0, 2.0, 3.0]

w.part.costs = [ 1.0, 2.0, 3.1 ]
# Part::_costs_changed [1.0, 2.0, 3.0] -> [1.0, 2.0, 3.1]
# update_costs: costs = [1.0, 2.0, 3.1]

w.part.costs[0] = 5.0
# Part::_costs_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007bd810>
# update_costs_items: costs_items = <traits.trait_handlers.TraitListEvent object at 0x1007bd810>

w.part.costs.append(4.0)
# Part::_costs_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007bd810>
# update_costs_items: costs_items = <traits.trait_handlers.TraitListEvent object at 0x1007bd810>

这种行为在文档中也有提到,具体可以查看这里

不过,如果使用了一个扩展的名称,那么在整个列表或列表中的某个项发生变化时,似乎可以调用同一个处理程序:

from traits.api import Float, HasTraits, Instance, List

class Part(HasTraits):
    costs = List(Float)

def _costs_changed(self, old, new):
    print("_costs_changed %s -> %s" % (str(old), str(new)))

def _costs_items_changed(self, old, new):
    print("_costs_items_changed %s -> %s" % (str(old), str(new)))

class Widget(HasTraits):
    part = Instance(Part)

    def __init__(self):
        self.part = Part()
        self.part.on_trait_change(self.update_costs, 'costs[]')  # <-- extended name

    def update_costs(self, name, new):
        print("update_costs: %s = %s" % (name, str(new),))

w = Widget()

w.part.costs = [ 1.0, 2.0, 3.0 ]
# _costs_changed [] -> [1.0, 2.0, 3.0]
# update_costs: costs = [1.0, 2.0, 3.0]

w.part.costs = [ 1.0, 2.0, 3.1 ]
# _costs_changed [1.0, 2.0, 3.0] -> [1.0, 2.0, 3.1]
# update_costs: costs = [1.0, 2.0, 3.1]

w.part.costs[0] = 5.0
# _costs_items_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007c6f90>
# update_costs: costs_items = [5.0]

w.part.costs.append(4.0)
# _costs_items_changed <undefined> -> <traits.trait_handlers.TraitListEvent object at 0x1007c6f90>
# update_costs: costs_items = [4.0]

在这种情况下,update_costs处理程序的name参数可以用来区分是列表本身发生了变化,还是列表中的某个项发生了变化。

5

在浏览他们的单元测试时,我发现了一个关于Dict特性的测试,来自enthought的事件单元测试覆盖...

当你有一个traits容器,比如traits.api.List()traits.api.Dict()时,你需要像这样设置一个神奇的事件监听器方法名称:

# Magic event listener for the `traits.api.List()` called self.volume_inputs
def _volume_inputs_items_changed(self, old, new):
def _volume_inputs_items_changed(self, old, new):
    # This is a static event listener for --> self.volume_inputs <--
    if len(new.added) > 0:
        print "Check it out, we added %s to self.items" % new.added
    elif len(new.removed) > 0:
        print "Check it out, we removed %s from self.items" % new.removed

同样,我还发现on_trait_change这个装饰器(用于动态的traits事件通知)如果你用traits.api.Listtraits.api.Dict来调用,也需要类似的命名方式……所以我也可以把上面的代码写成:

from traits.api import on_trait_change
# ...
@on_trait_change('volume_inputs_items')
def something_changed(self, name, new):
    # This is a static event listener for --> self.volume_inputs <--
    if len(new.added) > 0:
        print "Check it out, we added %s to self.items" % new.added
    elif len(new.removed) > 0:
        print "Check it out, we removed %s from self.items" % new.removed

无论哪种方式,当我运行代码时,我得到了预期的输出:

[mpenning@Bucksnort ~]$ python spinaltap.py
    - --> Firing _volume_inputs_items_changed() <-- check it out!!
- INITIAL_VALUE var volume_inputs = [5.0]
- APPEND a new volume of 4.0
    - volume_inputs = [5.0] # BEGIN
    - volume_inputs.append(4.0)
    - --> Firing _volume_inputs_items_changed() <-- check it out!!
    - volume_inputs: [5.0, 4.0] # END
    - Test PASSED: Traits fired _volume_inputs_changed()
- NEGATIVE Test... try to append 12.0.  This should fail; 12.0 is out of bounds
    - volume_inputs: [5.0, 4.0] # BEGIN
    - volume_inputs.append(12.0)
    - TraitError raised --> HERE <--
    - volume_inputs: [5.0, 4.0] # END
    - Test PASSED: traits correctly raised TraitError instead of appending 12.0.
[mpenning@Bucksnort ~]$

撰写回答