ActiveState Code

Recipe 52289: Multicasting on objects


Use the 'Multicast' class to multiplex messages/attribute requests to objects which share the same interface.

Python
 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"

    

Discussion

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:

print multicast.aClassAttribute.aMethod()

This will call 'aMethod' on 'aClassAttribute' from all delegation targets.

Comments

  1. 1. At 1 p.m. on 18 aug 2001, Anonymous said:

    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__ ?

  2. 2. At 11:31 a.m. on 13 oct 2001, Alex Martelli said:

    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"...!

  3. 3. At 11:58 a.m. on 23 dec 2001, Eduard Hiti (the author) said:

    Name change. You are right: Delegate is not the best name for this class. I'm taking up your suggestion 'Multicast'.

  4. 4. At 12:29 p.m. on 23 dec 2001, Eduard Hiti (the author) said:

    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

    multicast.method()
    

    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:

    if multicast.closed: print 'Closed'
    

    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.

  5. 5. At 2 p.m. on 13 apr 2004, Matthew Barnes said:

    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:

    def __setattr__(self, name, value):
        for object in self.values():
            setattr(object, name, value)
    

    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:

    def __call__(self, *args, **kwargs):
        import threading
        lock = threading.RLock()
        result = {}
    
        def invoke(alias, object):
            value = object(*args, **kwargs)
            lock.acquire()
            result[alias] = value
            lock.release()
    
        threadlist = [threading.Thread(
                      target=invoke, args=item)
                      for item in self.items()]
        for thread in threadlist:
            thread.start()
        for thread in threadlist:
            thread.join()
        return self.__class__(result)
    
  6. 6. At 9:35 a.m. on 18 jun 2004, Rick Price said:

    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.

        def __setattr__(self, name, value):
            if name == "data":
                self.__dict__[name]=value
                return
            for object in self.values():
                setattr(object, name, value)
    <pre>
    

    </pre>

  7. 7. At 5:51 p.m. on 26 jul 2009, Sudhi Herle said:

    Here is the version for Python 2.4+. This code does not use UserDict; instead it derives from the builtin dict:

    import operator
    
    class multicast(dict):
        "Class multiplexes messages to registered objects"
    
        def __init__(self, objs=[]):
            super(multicast, self).__init__()
            for alias, obj in objs:
                self.setdefault(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.items() if callable(obj) ] )
    
        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.values(), 1))
    
        def __getattr__(self, name):
            "Wrap requested attributes for further processing"
            return self.__class__( [ (alias, getattr(obj, name) ) \
                    for alias, obj in self.items() if hasattr(obj, name) ] )
    
        def __setattr__(self, name, value):
            """Wrap setting of requested attributes for further
            processing"""
    
            for o in self.values():
                o.setdefault(name, value)
    
    
    
    if __name__ == "__main__":
        import StringIO
    
        file1 = StringIO.StringIO()
        file2 = StringIO.StringIO()
    
        m = multicast()
        m[id(file1)] = file1
        m[id(file2)] = file2
    
        assert not m.closed
    
        m.write("Testing")
        assert file1.getvalue() == file2.getvalue() == "Testing"
    
        m.close()
        assert m.closed
    
        print "Test complete"
    

Sign in to comment