【问题标题】:Catching changes to a mutable attribute in python在python中捕获对可变属性的更改
【发布时间】:2013-11-21 02:19:43
【问题描述】:

每当属性发生变化时,我都会使用属性来执行一些代码,如下所示:

class SomeClass(object):
    def __init__(self,attr):
        self._attr = attr

    @property
    def attr(self):
        return self._attr

    @attr.setter
    def attr(self,value):
        if self._attr != value:
            self._on_change()
        self._attr = value

    def _on_change(self):
        print "Do some code here every time attr changes"

这很好用:

>>> a = SomeClass(5)
>>> a.attr = 10
Do some code here every time attr changes

但如果我将可变对象存储在 attr 中,则可以直接修改 attr,绕过设置器和我的更改检测代码:

class Container(object):
    def __init__(self,data):
        self.data = data

>>> b = SomeClass(Container(5))
>>> b.attr.data = 10
>>>

假设attr 只用于存储Container 类型的对象。有没有一种优雅的方法来修改SomeClass 和/或Container 以使SomeClassContainer 中存储的Container 对象被修改时执行_on_change?换句话说,我希望我的输出是:

>>> b = SomeClass(Container(5))
>>> b.attr.data = 10
Do some code here every time attr changes

【问题讨论】:

  • 你不能在你的 @attr.setter 方法中这样做吗?
  • @martineau -- 不,这里没有设置attrattr.data 是正在设置的内容。
  • 顺便说一下,enthought Traits 直接支持这种 notify-on-change 行为,包括在子对象被修改时通知父对象。这对这个应用程序来说是多余的,但如果你经常使用这样的复杂通知模式,它可能会很有用。

标签: python


【解决方案1】:

这是另一种解决方案。某种代理类。您不需要修改任何类来监控其中的属性变化,只需使用 ovverriden _on_change 函数将对象包装在 ChangeTrigger 派生类中:

class ChangeTrigger(object):
    def __getattr__(self, name):
        obj = getattr(self.instance, name)

        # KEY idea for catching contained class attributes changes:
        # recursively create ChangeTrigger derived class and wrap
        # object in it if getting attribute is class instance/object

        if hasattr(obj, '__dict__'):
            return self.__class__(obj)
        else:
            return obj 

    def __setattr__(self, name, value):
        if getattr(self.instance, name) != value:
            self._on_change(name, value)
        setattr(self.instance, name, value)

    def __init__(self, obj):
        object.__setattr__(self, 'instance', obj)

    def _on_change(self, name, value):
        raise NotImplementedError('Subclasses must implement this method')

例子:

class MyTrigger(ChangeTrigger):
    def _on_change(self, name, value):
        print "New value for attr %s: %s" % (name, value)

class Container(object):
    def __init__(self, data):
        self.data = data

class SomeClass(object):
    attr_class = 100
    def __init__(self, attr):
        self.attr = attr
        self.attr_instance = 5


>>> a = SomeClass(5)
>>> a = MyTrigger(a)
>>>
>>> a.attr = 10
New value for attr attr: 10
>>> 
>>> b = SomeClass(Container(5))
>>> b = MyTrigger(b)
>>> 
>>> b.attr.data = 10
New value for attr data: 10
>>> b.attr_class = 100        # old value = new value
>>> b.attr_instance = 100
New value for attr attr_instance: 100
>>> b.attr.data = 10          # old value = new value
>>> b.attr.data = 100
New value for attr data: 100

【讨论】:

  • 这个解决方案比公认的解决方案好得多,我建议改用这个。
  • 有一点,您不应该将触发器类中的 getattr 改为 getattribute 吗?
  • @raven __getattr__ 仅使用触发器类本身不存在的属性调用 - 假设此属性在包含的实例中。这是示例,也许在某些情况下您应该改用__getattribute__...
【解决方案2】:

如果您想跟踪更改并且不想在不同的类中使用on_change() 方法,您可以按照从here 开始的方式使用functools.partial

这样您可以包装您的数据并完全隐藏它。获取/更改只能通过该对象内部的一些方法来实现。

NB:python 没有私有属性,并且按照惯例,我们都是成年人并且违反规则行事。在您的情况下,您的 api 用户不应直接更改容器上的数据(创建后)。

注意:这里是为那些可能对其他方式感兴趣的人...

【讨论】:

  • 你对大人的事情提出了一个很好的观点。但在这种情况下,我确实希望我的用户从data 读取,我只是不希望他们在将其添加到SomeClass 对象后对其进行写入。当一个属性是私有的时,很容易用下划线表示。您如何指示属性应被视为只读?
  • 这里的 Python 约定是PEP 8。可以在this SO question 中找到其中的摘录。
  • AFAIK,PEP 8 没有为我们提供只读变量的约定。我的理解是,只读变量通常是通过创建一个非公共变量(带有下划线)然后使用属性提供公共的只读访问来实现的。我认为这符合我在这个问题中试图做的事情。当然,变量仍然不是真正的只读变量,用户可以随时选择直接写入非公共变量。
  • 当然不是。正如您所说,该变量仍然不是只读的。正如 PEP 8 中所说的,下划线是“内部”变量,双下划线变量可以被认为是“私有的”。它绝对符合你想要做的事情:)
  • functools.partial 方式的链接断开,悲伤的脸
【解决方案3】:

这里是 SomeClassContainer 的一个版本,我认为它具有您正在寻找的行为。这里的想法是对Container 的修改将调用与之关联的SomeClass 实例的_on_change() 函数:

class Container(object):
    def __init__(self, data):
        self.data = data

    def __setattr__(self, name, value):
        if not hasattr(self, name) or getattr(self, name) != value:
            self.on_change()
        super(Container, self).__setattr__(name, value)

    def on_change(self):
        pass

class SomeClass(object):
    def __init__(self, attr):
        self._attr = attr
        self._attr.on_change = self._on_change

    @property
    def attr(self):
        return self._attr

    @attr.setter
    def attr(self,value):
        if self._attr != value:
            self._on_change()
        self._attr = value

    def _on_change(self):
        print "Do some code here every time attr changes"

例子:

>>> b = SomeClass(Container(5))
>>> b.attr.data = 10
Do some code here every time attr changes
>>> b.attr.data = 10     # on_change() not called if the value isn't changing
>>> b.attr.data2 = 'foo' # new properties being add result in an on_change() call
Do some code here every time attr changes

请注意,对SomeClass 的唯一更改是__init__() 中的第二行,为了完整性和易于测试,我只是包含了完整的代码。

【讨论】:

  • 只要可以修改 Container 类就太好了,不幸的是,这排除了它直接与任何可变的内置容器类(如 dictlist 等)一起使用的可能性。跨度>
猜你喜欢
  • 2011-04-18
  • 2011-01-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-29
  • 2016-12-11
  • 2015-03-23
相关资源
最近更新 更多