Use the 'Multicast' class to multiplex messages/attribute requests to objects which share the same interface.
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
import operator from UserDict import UserDict class Multicast(UserDict): "Class multiplexes messages to registered objects" def __init__(self, objs=): UserDict.__init__(self) for alias, obj in objs: self.data[alias] = obj def __call__(self, *args, **kwargs): "Invoke method attributes and return results through another Multicast" return self.__class__( [ (alias, obj(*args, **kwargs) ) for alias, obj in self.data.items() ] ) def __nonzero__(self): "A Multicast is logically true if all delegate attributes are logically true" return operator.truth(reduce(lambda a, b: a and b, self.data.values(), 1)) def __getattr__(self, name): "Wrap requested attributes for further processing" return self.__class__( [ (alias, getattr(obj, name) ) for alias, obj in self.data.items() ] ) if __name__ == "__main__": import StringIO file1 = StringIO.StringIO() file2 = StringIO.StringIO() multicast = Multicast() multicast[id(file1)] = file1 multicast[id(file2)] = file2 assert not multicast.closed multicast.write("Testing") assert file1.getvalue() == file2.getvalue() == "Testing" multicast.close() assert multicast.closed print "Test complete"
A 'Multicast' object will expose the same interface as the delegation targets (Multicasting won't work for the dictionary interface, since that is used by the 'Multicast' class itself.)
Attributes of individual delegates can be accessed by the alias name used to register them for delegation:
multicast["test"] = aClass() print multicast.aClassAttribute["test"]
Message chains are possible:
This will call 'aMethod' on 'aClassAttribute' from all delegation targets.
Discussion too short. Cool example! Please discuss more in-depth what you're doing, i.e. what for are you using UserDict, __call__, __nonzero__, and __getattr__ ?
please do beef this up AND rename it...! definitely needs more discussion, particularly a critical examination of the anything-but-obvious design choices, yet the basic idea seems just fine... at least when renamed sensibly, e.g. to "Multicast" rather than the very generic and imprecise "Delegate"...!
Name change. You are right: Delegate is not the best name for this class. I'm taking up your suggestion 'Multicast'.
More discussion. The reason for UserDict subclassing is that there has to be a way to register delegation targets on this class. I could have used a 'register' method and, to be complete, an 'unregister' method and then an 'exists' method and so on, but I wanted to leave the class short and crisp, so the bookkeeping gets done via the convenient dictionary interface.
__getattr__ returns a Multicast that wraps the object attributes for which the multicasting should happen. Since Multicast has no attributes of its own (other than UserDict's), this meta-method gets called every time an attribute is accessed. So every attribute of Multicast is yet another Multicast.
And this is why there is a __call__ method. If you want to make a multicasting method call like
you first get a Multicast object returned via __getattr__ which wraps all 'method'-attributes of your delegation targets. After that the __call__ meta-method is invoked which will provide the real method calls.
__nonzero__ is there to make Multicasts usable in logical contexts:
will print 'Closed' if all 'closed'-attributes of your delegation targets are true. The operator.truth call in __nonzero__ is needed because an integer must be returned (doing otherwise is a TypeError).
The example gives a hint how I came to write this class. I had a bunch of log files which needed to get the same messages, and Multicast did this quite fine.
Suggested Enhancements. I've found this recipe to be very useful and have added a couple enchancements which I thought I'd share.
First of all, multicasting the setting of attributes is very straight-forward since direct assignment is not normally used in underlying dictionary class:
Secondly, the operations being invoked through __call__ may take a long time to complete. Here's a way to run them concurrently and produce the same result as the original recipe:
Fix for __setattr__. If you use the __setattr__ as described above you get infinite recursion errors on Python 2.3.4 when you create an instance of Multicast with no parameters (at least). The fix below, which I got from Blake Winton, fixes the problem.
Here is the version for Python 2.4+. This code does not use UserDict; instead it derives from the builtin dict:
Here is an simpler, 'more readable' implementation, which multicasts method calls to a number of observer objects.
Use it like this:
and the output is:
P.S. Note that this particular implementation is a little bit less powerful, as it does not multicast the setting of attributes, so you can't do "observers.someattr = 5". However I am open to suggestions on how to do that, building upon the simpler implementation I am putting forward here.