This class is designed to call a similar function of multiple objects of multiple classes using a single interface (it is not for calling functions that return a value).

A potential usage will be sending same data to multiple writers (log writer, HTML generator and styled printer to console).

Tested with Python 3.4.2

I've also included a test/example in the below code:

"""
caller module:
Contains Caller class - call multiple other classes or modules with
similar function definitions, (it is not for calling functions that 
returns a value)

Author : Bhathiya Perera
"""
class Caller():
    """call other classes or modules with
       similar function definitions"""
    def __init__(self, *receivers):
        """Initialize

        Parameters :                    
            receivers - va-arg receivers (can be objects, modules,
                        another caller , ....)    
        """
        self._names = []
        self._receivers = receivers

    def __getattr__(self, name):
        """Get attribute of a given name
        This will return 'self' therefore it can be called later
        """    
        self._names.append(name)
        return self

    def __call__(self, *args, **kw):
        """This class is callable with any arguments or key-value arguments
        It will then be posted to all receivers
        """
        if len(self._names) == 0:
            raise Exception("Cannot call")

        method_name = self._names.pop()
        for receiver in self._receivers:
            method = getattr(receiver, method_name)
            method(*args, **kw)

        return self

if __name__ == "__main__":
    # -------------------------------------------------
    # Test its usage
    class Receiver1():
        def a(self, arg):
            print ("Receiver1 - a", arg)

        def b(self, arg):
            print ("Receiver1 - b", arg)       

    class Receiver2():
        def a(self, arg):
            print ("Receiver2 - a", arg)

        def b(self, arg):
            print ("Receiver2 - b", arg)

    class Receiver3():
        def a(self, arg):
            print ("Receiver3 - a", arg)

        def b(self, arg):
            print ("Receiver3 - b", arg)

    c = Caller(Receiver3())
    d = Caller(Receiver1(), Receiver2(), c)  
    d.a("hello a")
    d.b("hello b")
    print ("-----")
    d.a.b.a.b.a.b.a("a")("b")("c")("d")("e")("f")("g")

If it was executed as a module it will print:

Receiver1 - a hello a
Receiver2 - a hello a
Receiver3 - a hello a
Receiver1 - b hello b
Receiver2 - b hello b
Receiver3 - b hello b
-----
Receiver1 - a a
Receiver2 - a a
Receiver3 - a a
Receiver1 - b b
Receiver2 - b b
Receiver3 - b b
Receiver1 - a c
Receiver2 - a c
Receiver3 - a c
Receiver1 - b d
Receiver2 - b d
Receiver3 - b d
Receiver1 - a e
Receiver2 - a e
Receiver3 - a e
Receiver1 - b f
Receiver2 - b f
Receiver3 - b f
Receiver1 - a g
Receiver2 - a g
Receiver3 - a g

Review for Python conventions and anything else.

up vote 2 down vote accepted

If I understand correctly, the purpose of Caller seems to be to call a function on multiple objects. It works like this:

  • Construct a Caller x by passing 1 or more objects in the constructor
  • Call any method m on x, and it will be dispatched to method m of all objects

Right? The docstring doesn't explain this very well. CallDispatcher might be a better name.

My impression is that you're discovering powerful features of Python, and you're trying to use them because you can, not because you have a concrete purpose. Sort of like an exercise, not for real-life use.

It seems to me that __getattr__ is seriously abused. I don't have experience overriding this method, but I would guess it's designed to implement getters dynamically. First of all, adding getters dynamically seems like a hack that should be used with extreme care, including a good justification that it's the best option. Secondly, instead of behaving as a getter, this implementation mutates the object and returns self. I see you did that to make chaining and currying possible, it's interesting, but it seems a misuse of the language.

Another thing I don't like about the approach is the heavy dependence on duck typing. The objects that can be used with Caller don't have to follow a well-defined interface, they can be anything. The user just has to make sure that when they call a .hello function on a Caller object, all the objects inside have a .hello function defined. It's great that Python let's us do this kind of thing, but it doesn't mean that we should. I prefer to have well-defined and well-documented interfaces, with a list of legitimate methods that I'm allowed to call.

Python conventions

Instead of:

class Caller():

The class should be declared as:

class Caller:

Instead of:

    if len(self._names) == 0:

The Pythonic way:

    if not self._names:

Instead of:

        print ("Receiver3 - b", arg)

There should be no space before the opening paren:

        print("Receiver3 - b", arg)
  • Thanks, I have been doing too much Python coding without getting any reviews. Also I have trouble writing module level doc-string, they just end up being a copy of Classes' doc strings. – Bhathiya Perera Dec 21 '14 at 17:39
  • 1
    There's no such thing as too much Python coding ;-) Keep it up! – janos Dec 21 '14 at 17:45

The idea of using __getattr__ to build a stack of methods to call feels strange to me, and your usage example d.a.b.a.b.a.b.a("a")("b")("c")("d")("e")("f")("g") does nothing to convince me otherwise. Also, if I do this

a = d.a
b = d.b
a("a")
b("b")

I get this result:

Receiver1 - b a
Receiver2 - b a
Receiver3 - b a
Receiver1 - a b
Receiver2 - a b
Receiver3 - a b

Instead, I suggest returning just a callable from __getattr__, eg. like this:

class Caller():

    def __init__(self, *receivers):
        self._receivers = receivers

    def __getattr__(self, method_name):
        def f(*args, **kw):
            for receiver in self._receivers:
                method = getattr(receiver, method_name)
                method(*args, **kw)   
        return f

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.