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

This is an dynamic dispatching approach inspired by C#-style events. The approach allows for the dispatch of an event to a series of chained methods through the use of a Dispatcher. The Dispatcher can be defined either as part of a class or merely as a variable in some code. When the Dispatcher is invoked the methods that are chained to it (i.e. handlers) are invoked. The dispatch can be either blocking or non-blocking, and is non-blocking by default.

Python, 81 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
# dispatch.py

# definitions:

import threading

class Dispatcher(object):
    def __init__(self, targets=None, nonBlocking=True):
        if not targets or targets is None:
            self._targets = []
        else:
            self._targets = targets
        self._nonBlocking = nonBlocking
    def __iadd__(self, target):
        self._targets.append(target)
        return self
    def __isub__(self, target):
        self._targets.remove(target)
        return self
    def isNonBlocking(self):
        return self._nonBlocking
    nonBlocking = property(isNonBlocking)
    def __call__(self, *listArgs, **kwArgs):
        def invokeTargets():
            for target in self._targets:
                target(*listArgs, **kwArgs)
        if self.nonBlocking:
            threading.Timer(0, invokeTargets).start()
        else:
            invokeTargets()

# demos:

def Test1():
    """
    A simple example demonstrating most functionality.
    """
    def m1():
        print 'm1 invoked'
    def m2():
        print 'm2 invoked'
    e = Dispatcher()
    e += m1
    e += m2
    e += m2
    print 'Dispatching:'
    e()
    e -= m1
    print 'Dispatching:'
    e()
    e -= m2
    print 'Dispatching:'
    e()

def Test2():
    """
    A more realistic example for the OO programmer.
    """
    class Sprite(object):
        def __init__(self, location):
            self._location = location
        locationChanged = Dispatcher()
        def getLocation(self):
            return self._location
        def setLocation(self, newLocation):
            oldLocation = self._location
            self._location = newLocation
            # Dispatch a "property change event"
            self.locationChanged(oldLocation, newLocation)
        location = property(getLocation, setLocation)
    s = Sprite((2,4))
    def SpriteLocationChanged(oldLocation, newLocation):
        print 'oldLocation =', oldLocation
        print 'newLocation =', newLocation
    s.locationChanged += SpriteLocationChanged
    s.location = (3,4)
    s.location = (4,4)

if __name__ == '__main__':
    Test1()
    Test2()

Having recently seen recipe http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/410686, I thought I would offer my approach.

Like the other recipe that I reference, mine is inspired by C#-style event handling. The difference between this and the C# approach is that I follow the python tradition of duck-typing where no explicit signatures are enforced. When you add a handler to the dispatch, it's up to you to keep the methods compatable with the arguments given in the dispatch. My recipe is similar to the C#-style events in that the handlers are non-blocking by default. That is, the calls to the handlers will not prevent the continuation of the code that issued the "dispatch".

This is different from the referenced recipe in that I think it is both simpler and more straightforward, in that it provides a simple implementation of event-style behavior that allows you to declare events where you want them without being forced to inherit, and handlers are easily added by client-code just as in C#. There are no required base-classes, no required class-slots, no required magic attributes. I'm choosing the simplicity of this approach over one that would require more forced introspection.

11 comments

Zoran Isailovski 18 years, 9 months ago  # | flag

Dispatcher === _EventSlot. As I see, you have basically followed Alexander Semenov's remark to my recipe (http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/410686 - see remark named "It is may be simpler", posted on 2005/05/07): Your Dispatcher class (which Alexander suggested naming Event) is basically my _EventSlot class spiced up with a bit of threading.

Originally, my Events class would be (and is) typically used like this:

class Sprite:
   events = Events()
...
   s = Sprite((2,4))
   def SpriteLocationChanged(oldLocation, newLocation):
      ...
   def SpriteImageChanged(oldImage, newImage):
      ...
   s.events.LocationChanged += SpriteLocationChanged
   s.events.ImageChanged += SpriteImageChanged

Note - No extra event declarations needed. Particular events are added automatically as they are referred. This was exactly what I wanted.

The magic __events__ came in in order to gain some control over events that are subscribed to but never fired. With this aspect in mind, I certainly agree that Alexanders proposal and your implementation is the simpler and safer approach.

Zoran Isailovski 18 years, 9 months ago  # | flag

Some hints. Simplify:

def __init__(self, targets=None, nonBlocking=True):
  self._targets = targets or []

Fix / make safer:

def __iadd__(self, target):
   ##if target in self._targets: return #<- (*)
   self._targets.append(target)
   return self
def __isub__(self, target):
   # __iadd__ allows for multiple addition of the same target,
   # so we need to remove ALL occurences of target here
   # At the same time, we gracefully ignore targets that are not here
   # Alternatively, ensure a target is added only once ==> see (*)
   while target in self._targets:
      self._targets.remove(target)
   return self
Zoran Isailovski 18 years, 9 months ago  # | flag

Remark on "C#-style events ... are non-blocking by default" Note that an event slot call in C# is definitely blocking, at least in WinForms applications on .NET 1.1.

Mark Cummings 18 years, 9 months ago  # | flag

Simplicity of implementation vs. simplicity of use. Just a few thoughts. I think the principle difference between this and the referenced recipe is that the former focuses on simplicity of design/implementation while the later focuses on ease of use within client code. I would certainly agree that it is easier to use the event dispatching mechanism provided by the referenced recipe, as only one declaration per "event set" is required, and events may be added without further declaration. One of the main design goals of the current recipe, however, is to implement the concept of dynamic dispatch without forcing the client code to group the objects being dispatched ("event", "message", whatever the necessary conceptualization may be) into one class whose sole purpose is to provide a (useful, to be sure) "bookkeeping" mechanism, which is the point I believe Alexander may have been making. I think this recipe is, as you said, simply a refinement of the _EventSlot class in the referenced recipe along the lines of Alexander's suggestions, with the added philosophy that the grouping of Dispatcher's within a separate class should merely be a design decision that is made within the client code, not within the implementation of the dispatching system.

With respect to introspection, I don't necessarily see the advantage of that the Events class provides. If a person would like to inspect the events that are currently available for a particular class, they would have to know something about the implementation of Events; namely, to look in the __name__ attribute of the contents of __event__. In the current recipe, however, one simply has to look for members of the class that are instances of Dispatcher, and not necessarily know anything about how Dispatcher is implemented.

Zoran Isailovski 18 years, 9 months ago  # | flag

You are right. In some situations I found it useful to loop over events

for event in obj.events:
   inspect(event)

However, these situations were rare, and the same could be acomplished with

for name, event in vars(obj).items():
   if not isinstance(event, Dispatcher): continue
   inspect(event)

The more I think about it, the more I like the simplifications here. ;)

Steven Cummings (author) 18 years, 9 months ago  # | flag

Re: non-blocking. Hmm... I was pretty sure of this. So I'll have to look into it pretty soon. In any case you can turn threading off, and you can change its default when you implement this in your own project.

Steven Cummings (author) 18 years, 9 months ago  # | flag

Re: I definitely still disagree here. I'm allowing that for each use of += with a handler, the programmer knows that he is adding it for the first time or not. Therefore, with the equivalent (and symmetric) -=, he knows he wants to remove one or more notifcations to a handler. I do not presume to know for every programmer how they want manage the possibility of multiple notifications, therefore, I leave it flexible.

Zoran Isailovski 18 years, 9 months ago  # | flag

straightforward != flexible. Accidental +=/-= asymmetry can lead to consecutive faults that are hard to track down to their real source. (For ex., a handler accidentally left in the dispatcher that accesses a destroyed widget might cause an exception saying that there's is no widget with id=xxx, which is a bit misleading about the real source of error.)

So, while your approach is straightforward, it is not flexible at all. On the contrary, my proposed "graceful release" is the really flexible approach: It is practically equivalent to yours in symmetric +=/-= situations, but handles asymmetric ones as well (instead of leaving the burden to the user of the code).

In my experience, it always paid off to go for a gracefull release.

Steven Cummings (author) 18 years, 9 months ago  # | flag

Re: Actually, in this case, yes it is. You seem to keep confusing the "conveniences" that you think must be enforced for all programmers as flexibility. Flexibility is defined as choices, and I'm electing to leave choices of how to manage handlers there. It's completely stated what is done. Further, the straightforwardness of the recipe is such that the user can change the bit of code as they wish to do what you say, or manage it outside. Given that, I guess it might be nice to implement the "in" operator. Your "graceful release" may be a convenience, and you can argue all day about whether or not it is more healthy for all programmers aside from yourself, but the objective bottom line is that enforcing it and removing choice is not flexibility. Remember, this is a recipe, not a standard module for which I'm going to attempt to make all choices and solve all problems for prospective useres. That's why it is flexible.

Steven Cummings (author) 18 years, 8 months ago  # | flag

non-blocking. Alrighty, it appears ALL events (not just within WinForms) are blocking on the .NET platform:

http://msdn.microsoft.com/msdnmag/issues/03/02/Multithreading/default.aspx

So I stand corrected on that. In any case it can be turned off or even easily excluded altogether in this recipe. But until I run into conflicts with any 3rd-party modules or frameworks I rather like the non-blocking option.

Zoran Isailovski 18 years, 7 months ago  # | flag

Re yourself :p. Firstly, I don't think you are able to assert whether or not I am confusing something. The utmost that you can tell is that you don't see the connexion.

Secondly, and more important, if I am removing choice, as you say, then what kind of choice are YOU leaving open? The choice to modify that piece of code? Well, THAT is a choice that we already always had, with or without your effort. Sorry to disillusion you, but hey, thanks anyway.

And now for the better: Although it is refreshing to see people defending their recipes this passionately (including myself - sigh - what imperfect creatures we really are :), you should admit that this kind of discussion easily gets quite destructive. Let's try to change that:

In one point I agree: This is a recipe, not a module. So your favor for straightforwardness over trickiness of design is comprehensible (however flexible and briliant that tricky design might be :p) But flexibility IS something else than a chance to change the code.

Anyway, I probably really could "argue all day", if you don't see the difference, then you don't see it. You'll know it once you experience it for yourself - just as with the blocking vs. non-blocking issue.

And now excuse me, I will make use of my freedom of choice to devote myself to more fruitful things... :D