Monitor sets for changes using the Observer design pattern.
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.
Tags: database
One thing I don't get: what's the purpose of the read-only access to "listeners"?
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.
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?