IMPROVED. The concept of events is heavily used in GUI libraries and is the foundation for most implementations of the MVC (Model, View, Controller) design pattern (the latter being my prime motivation for this recipe). Another prominent use of events is in communication protocol stacks, where lower protocol layers need to inform upper layers of incoming data and the like. Here is a handy class that encapsulates the core to event subscription and event firing and feels like a "natural" part of the language.
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 | ######################################################################
##
## Events
##
######################################################################
class Events:
def __getattr__(self, name):
if hasattr(self.__class__, '__events__'):
assert name in self.__class__.__events__, \
"Event '%s' is not declared" % name
self.__dict__[name] = ev = _EventSlot(name)
return ev
def __repr__(self): return 'Events' + str(list(self))
__str__ = __repr__
def __len__(self): return NotImplemented
def __iter__(self):
def gen(dictitems=self.__dict__.items()):
for attr, val in dictitems:
if isinstance(val, _EventSlot):
yield val
return gen()
class _EventSlot:
def __init__(self, name):
self.targets = []
self.__name__ = name
def __repr__(self):
return 'event ' + self.__name__
def __call__(self, *a, **kw):
for f in self.targets: f(*a, **kw)
def __iadd__(self, f):
self.targets.append(f)
return self
def __isub__(self, f):
while f in self.targets: self.targets.remove(f)
return self
######################################################################
##
## Demo
##
######################################################################
if __name__ == '__main__':
class MyEvents(Events):
__events__ = ('OnChange', )
class ValueModel(object):
def __init__(self):
self.events = MyEvents()
self.__value = None
def __set(self, value):
if (self.__value == value): return
self.__value = value
self.events.OnChange()
##self.events.OnChange2() # would fail
def __get(self):
return self.__value
Value = property(__get, __set, None, 'The actual value')
class SillyView(object):
def __init__(self, model):
self.model = model
model.events.OnChange += self.DisplayValue
##model.events.OnChange2 += self.DisplayValue # would raise exeception
def DisplayValue(self):
print self.model.Value
model = ValueModel()
view = SillyView(model)
print '\n--- Events Demo ---'
# Events in action
for i in range(5):
model.Value = 2*i + 1
# Events introspection
print model.events
for event in model.events:
print event
|
The C# language provides a handy way to declare, subscribe to and fire events. I wanted something similar in python, and this recipe is the result.
Technically, an event is a "slot" where callback functions (event handlers) can be attached to - a process referred to as subscribing to an event. To subscribe to an event:
events = Events()
...
events.OnChange += MyEventHandlerForTheOnChangeEvent
When the event is fired, all attached event handlers are invoked in sequence. To fire the event, perform a call on the slot:
events.OnChange()
Usually, instances of Events will not hang around loosely like above, but will typically be embedded in model objects, like here:
class MyModel(object):
def __init__(self):
self.events = Events()
...
Similarly, view and controller objects will be the prime event subscribers:
class MyModelView(SomeWidget):
def __init__(self, model):
...
self.model = model
model.events.OnChange += self.DisplayValue
...
def DisplayValue(self):
...
The Events and _EventSlot classes provide some introspection support, too:
- Every event (aka event slot) has a __name__ attribute
- You can iterate through all registered events in Events instances
This is usefull for ex. for an atomatic event subscription based on method name patterns.
Note that the recipe does not check if an event that is being subscribed to can actually be fired, unless the class attribute __events__ is defined. This can cause a problem if an event name is slightly misspelled. If this is an issue, subclass Events and list the possible events, like:
class MyEvents(Events):
__events__ = ('OnThis', 'OnThat', ...)
Cheers and happy event handling!
See Also
- Recipe 52289, "Multicasting on objects", by Eduard Hiti shows a simple Multicast implementation (renamed to Multiplex in the 1st edition of the Cookbook).
- This recipe differs from Multiplex in that arbitrarily named event handlers are bound to an arbitrarily named event, whereas Mutliplex dispatches a call of the form multiplex.f() to the method f in every object in the Multiplex.
Nice and practical. Nice and practical, easy to use recipe.
It is may be simpler. Why class Events and it's derivatives with all of this magic is needed? Only to steal event's name and pass it to _EventSlot constructor? I think _EventSlot (better renamed to 'event') is enough in most cases:
weakevents. I think it's interesting to add support of "weak events" (weak references to handlers) so handlers may die when not needed anymore
Simpler implementation here means complication for client code. The class Events is there mainly for 3 reasons:
Events (Slots) are added automatically, so there is no need to declare/create them separately. This is great for prototyping. (Note that __events__ is optional and should primarilly help detect misspelled event names.)
To provide (and encapsulate) some level of introspection.
And yes, to "steel the name" and hereby remove unneeded redundancy in a call like
xxx.OnChange = event('OnChange')
Not really. I did implement weak events for fun, and they are not helpful at all.
If you think further, adding weak event handlers would only prevent observers from beeing kept alive by their subject alone. But, since observers are usually GUI elements, their life is determined by user actions, not their reference count. When the user closes a window, for ex., the underlying widget/handle/whatever is destroyed, even if the wrapping python object continues to exist. Now, whether there are references to the python object or not - it becomes practically unusable.
So what you actually need is to unsubscribe from the subject whenever the underlying widget goes away, but no weakrefs will help you here.