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

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

Python, 43 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
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:

print multicast.aClassAttribute.aMethod()

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

8 comments

rbeer 22 years, 8 months ago  # | flag

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

Alex Martelli 22 years, 6 months ago  # | flag

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

Eduard Hiti (author) 22 years, 4 months ago  # | flag

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

Eduard Hiti (author) 22 years, 4 months ago  # | flag

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.

Matthew Barnes 20 years ago  # | flag

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)
Rick Price 19 years, 10 months ago  # | flag

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>

Sudhi Herle 14 years, 9 months ago  # | flag

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"
Andy Bulka 12 years ago  # | flag

Here is an simpler, 'more readable' implementation, which multicasts method calls to a number of observer objects.

class multicast:
    def __init__(self):
        self.objects = []

    def add(self, o):
        self.objects.append(o)

    # needs to return a callable function which will then be called by python,
    # with the arguments to the original 'method' call.
    def __getattr__(self, method): 
        def broadcaster(*args, **kwargs):
            for o in self.objects:
                func = getattr(o, method, None)
                if func and callable(func):
                    func(*args, **kwargs)
        return broadcaster

Use it like this:

class Fred:
    ugg = 1
    def hi(self):
        print "hi from Fred"
    def count(self, n):
      for i in range(n): print i

class Mary:
    def hi(self):
        print "hi from Mary"
    def count(self, n):
      for i in range(n): print i+100

observers = multicast()
observers.add(Fred())
observers.add(Mary())
observers.hi()
observers.count(5)
observers.ugg()   # ugg method doesn't exist, so no calls made

print "done"

and the output is:

hi from Fred
hi from Mary
0
1
2
3
4
100
101
102
103
104
done

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.