在python中捕获对可变属性的更改

Catching changes to a mutable attribute in python

每次属性发生更改时,我都使用属性来执行一些代码,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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"

这很有效:

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

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

1
2
3
4
5
6
7
class Container(object):
    def __init__(self,data):
        self.data = data

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

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

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


这里有一个版本的SomeClassContainer,我认为这是你想要的行为。这里的想法是,对Container的修改将调用与之相关的SomeClass实例的_on_change()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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"

例子:

1
2
3
4
5
6
>>> 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__()中的第二行,我只是包含了完整性和易于测试的完整代码。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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')

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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


如果您想跟踪更改,并且不想在不同的类中处理与on_change()方法的杂耍,可以使用functools.partial,从这里开始。

这样,您就可以包装数据并完全隐藏它。获取/更改只能通过在该对象中融合的某些方法实现。

注意:python没有私有财产,按照惯例,我们都是成年人,并且违反规则。在您的情况下,API的用户不应该直接更改容器上的数据(在创建之后)。

注:这里是给那些可能对其他方式感兴趣的人……