内存使用@on_trait_change vs _foo_changed()

memory usage @on_trait_change vs _foo_changed()

我确实用 Enthought Traits 构建了一个应用程序,它占用了太多内存。我认为,问题是由特征通知引起的:

@on_trait_change 或使用特殊命名约定(例如 _foo_changed() )捕获的事件在内存使用方面似乎存在根本差异。我用两个 类 Foo 和 FooDecorator 做了一个小例子,我假设它们表现出完全相同的行为。但他们没有!

from traits.api import *

class Foo(HasTraits):
    a = List(Int)

    def _a_changed(self):
        pass

    def _a_items_changed(self):
        pass

class FooDecorator(HasTraits):
    a = List(Int)

    @on_trait_change('a[]')
    def bar(self):
        pass

if __name__ == '__main__':
    n = 100000
    c = FooDecorator
    a = [c() for i in range(n)]

当运行此脚本使用 c = Foo 时,Windows 任务管理器显示整个 python 进程的内存使用量为 70MB,随着 n 的增加保持不变。对于 c = FooDecorator,python 进程使用 450MB,随着 n 的增加而增加。

你能给我解释一下这种行为吗?

编辑:也许我应该换个说法:为什么有人会选择 FooDecorator 而不是 Foo?

编辑 2:我刚刚卸载了 python(x,y) 2.7.9 并安装了具有 traits 4.5.0 的最新版本的 canopy。现在 450MB 变成了 750MB。

编辑 3:我自己编译了 traits-4.6.0.dev0-py2.7-win-amd64。结果与 EDIT 2 中的结果相同。因此,尽管所有合理性 https://github.com/enthought/traits/pull/248/files 似乎都不是原因。

我相信您看到了最近已修复的内存泄漏的影响: https://github.com/enthought/traits/pull/248/files

至于为什么要使用装饰器,在这个特定的例子中,这两个版本实际上是等价的。

总的来说,装饰器更灵活:你可以给出要监听的特征列表,也可以使用扩展名称表示法,如下所述: http://docs.enthought.com/traits/traits_user_manual/notification.html#semantics

例如,在这种情况下:

class Bar(HasTraits):
    b = Str

class FooDecorator(HasTraits):
    a = List(Bar)

    @on_trait_change('a.b')
    def bar(self):
        print 'change'

bar 通知程序将被调用以更改特征 a、其项目,以及每个 [=14] 中特征 b 的更改=] 项。扩展名可以非常强大。

这里发生的事情是 Traits 有两种不同的处理通知的方式:静态通知器和动态通知器。

静态通知器(例如那些由特别命名的 _*_changed() 方法创建的通知器)相当轻量级:实例上的每个特征都有一个关于 t 的通知器列表,这些通知器基本上是函数或方法使用轻量级包装器。

动态通知器(例如使用 on_trait_change()a[] 之类的 extended trait name conventions 创建的通知器明显更加强大和灵活,但因此它们更加重量级。特别是,除了他们创建的包装器对象之外,他们还创建了扩展特征名称的解析表示和处理程序对象,其中一些又是 HasTraits 子类实例。

因此,即使是像 a[] 这样的简单表达式,也会创建相当数量的新 Python 对象,并且必须为每个 on_trait_change 创建这些对象分别监听每个实例,以正确处理实例特征等极端情况。相关代码在这里:https://github.com/enthought/traits/blob/master/traits/has_traits.py#L2330

根据报告的数字,您看到的大部分内存使用差异在于为每个实例和每个 on_trait_change 装饰器创建此动态侦听器基础结构。

值得注意的是,在您使用简单特征名称的情况下,on_trait_change 存在短路,在这种情况下,它会生成静态特征通知程序而不是动态通知程序。所以如果你改为写这样的东西:

class FooSimpleDecorator(HasTraits):
    a = List(Int)

    @on_trait_change('a')
    def a_updated(self):
        pass

    @on_trait_change('a_items')
    def a_items_updated(self):
        pass

您应该会看到与特别命名的方法相似的内存性能。

要回答关于 "why use on_trait_change" 的改写问题,在 FooDecorator 中,如果您对列表或列表中任何项目的更改的响应相同,则可以编写一个方法而不是两个.这使得代码更容易调试和维护,如果您没有创建数千个这样的对象,那么额外的内存使用量可以忽略不计。

当您考虑更复杂的扩展特征名称模式时,这将成为一个更重要的因素,其中动态侦听器自动处理更改,否则需要大量手动(且容易出错)代码来从中间连接和删除侦听器对象和特征。这种方法的强大功能和简单性通常胜过对内存使用的担忧。