Welcome, guest | Sign In | My Account | Store | Cart

Monitor sets for changes using the Observer design pattern.

Python, 99 lines
 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
class ListenableSet(set):
    '''A set that notifies listeners whenever it is updated

    Callback order and signature:

        for cb in self.listeners:  cb(self)

    '''

    def __init__(self, *args, **kwds):
        self._listeners = []
        set.__init__(self, *args, **kwds)

    def notify(self):
        'Run all the callbacks in self.listeners.'
        for cb in self._listeners:
            cb(self)

    @property
    def listeners(self):
        'Read-only access to _listeners'
        return self._listeners

    def _detect_size_change(methodname, parent):
        method = getattr(parent, methodname)
        def wrapper(self, *args, **kwds):
            original_size = len(self)
            result = method(self, *args, **kwds)
            if len(self) != original_size:
                for cb in self._listeners:
                    cb(self)
            return result
        wrapper.__name__ = methodname
        wrapper.__doc__ = method.__doc__
        return wrapper

    def _detect_any_call(methodname, parent):
        method = getattr(parent, methodname)
        def wrapper(self, *args, **kwds):
            result = method(self, *args, **kwds)
            for cb in self._listeners:
                cb(self)
            return result
        wrapper.__name__ = methodname
        wrapper.__doc__ = method.__doc__
        return wrapper

    for methodname in 'clear add pop discard remove update difference_update ' \
                      '__isub__ __iand__ __ior__ intersection_update'.split():
        locals()[methodname] = _detect_size_change(methodname, set)
    for methodname in '__ixor__ symmetric_difference_update'.split():
        locals()[methodname] = _detect_any_call(methodname, set)



# ---- Example callback to print information on size -------------------------

def notice(s):
    print 'Set at %d now has size %d\n' % (id(s), len(s))

s = ListenableSet('abcdefgh')
s.listeners.append(notice)
s.add('i')      # adding a new element changes the set and triggers a callback
s.add('a')      # adding an existing element results in no change or callback
s.listeners.remove(notice)
s.add('j')      # without a listener, works just like a regular set


# ---- Example callback to report changes in the set -------------------------

def change_report(s, prev=set()):
    added = s - prev
    if added:
        print 'Added elements: ', list(added)
    removed = prev - s
    if removed:
        print 'Removed elements: ', list(removed)
    if added or removed:
        print
    prev.clear()
    prev.update(s)

s = ListenableSet()
s.listeners.append(change_report)
s.update('abracadabra')
s ^= set('simsalabim')


# ---- Example callback to enforce a set invariant ---------------------------

def enforce_lowercase(s):
    for elem in s:
        if not elem.islower():
            raise ValueError('Set member must be lowercase: ' + repr(elem))

s = ListenableSet()
s.listeners.append(enforce_lowercase)
s.add('slartibartfast')
s.add('Bang')   # Raises an exception because the set element isn't lowercase

Idea proposed by Jason Wells.

3 comments

Matteo Dell'Amico 16 years, 11 months ago  # | flag

One thing I don't get: what's the purpose of the read-only access to "listeners"?

Raymond Hettinger (author) 16 years, 11 months ago  # | flag

Why listeners is read-only. The listeners attribute is a list. We let client's mutate the list by adding or removing listeners, but they have need to replace the list with a different object. And, knowing that the listeners list object is not changeable, it can be shared by clients across multiple threads.

Peter Russell 15 years, 1 month ago  # | flag

Updating locals(). I see that you add decorated versions of some of the set methods to the class suite by assigning to the locals() dictionary. Obviously this works, but since the library reference says that you should treat locals() as read only, do you know if this is a quirk of CPython, or is it part of the language?

Created by Raymond Hettinger on Tue, 22 May 2007 (PSF)
Python recipes (4591)
Raymond Hettinger's recipes (97)

Required Modules

  • (none specified)

Other Information and Tasks