6
\$\begingroup\$

In attempting to decouple the object from the messaging I came up with the following short code for adding publish-subscribe functionality to a Python script.

from collections import defaultdict

Topics = defaultdict(set)

def subscribe(obj, message):
    """Adds key-value pairs to the Topics dict.
    Keys are messages, values are placed in a set."""
    Topics[message].add(obj)

def unsub(obj, message):
    """Removes a subscriber from a message."""
    Topics[message].remove(obj)

def unsub_all(obj):
    for message in Topics:
        if obj in Topics[message]:
            Topics[message].remove(obj)

def publish(message, *args, **kwargs):
    """Sends message to subscribers."""
    for obj in Topics[message]:
        getattr(obj, message)(*args, **kwargs)

def publish_with_results(message, *args, **kwargs):
    """Same as publish, but returns list of results."""
    return [getattr(obj, message)(*args, **kwargs) for obj in Topics[message]]

There's no Message or Signal object; objects in question need only (a) be subscribed, and (b) have a matching method name. So for example:

from stickpubsub import subscribe, publish

class A(object):
    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value
        publish('x_was_set', self._x)

class B(object):
    def __init__(self):
        subscribe(self, 'x_was_set')

    def x_was_set(self, value):
        if getattr(self, 'last_x', False):
            print "The last x I saw was {}.".format(self.last_x)
        else:
            print "I guess I wasn't around for the last x."
        self.last_x = value
>>> Dave = A(15)
>>> Jupiter = B()
>>> Dave.x = 17
I guess I wasn't around for the last x.
>>> Dave.x *= 2
The last x I saw was 17.

Objects can of course be subscribed to events outside their __init__ call.

This is actually an early version; I've a few different implementations - one that uses proxy objects in the stead of just adding the object to a set, another that separates the message from the methods, and so on. This just seemed like a nice and short example to put out there for the purpose of chewing the fat.

Primarily I'm curious as to what I might be "missing out on" by not expressly having a Message() or Signal() or Event() object, as I see many other 'pubsub example' code snippets employ. There's no attempt to obfuscate the kinds of errors being thrown or to replace one error with a frivolous custom exception, and the whole thing takes like 30 lines and seems to be fairly fast. The only thing I can see creating another object doing is adding another point of failure, but perhaps I'm just naïve!

So what is the upshot of an Event-like object in such a module? Also, is my code alright?

\$\endgroup\$
4
+25
\$\begingroup\$

1. Code review

It's hard to evaluate this kind of thing in the abstract without knowing what the requirements are. However, here are a few thoughts.

  1. There's no top-level documentation. How am I supposed to use this code? You could provide a module-level docstring.

  2. The Python style guide (PEP8) recommends that global variables should be "lowercase, with words separated by underscores". So topics would be preferred to Topics (which looks like the name of a class). You're not obliged to follow PEP8, but it makes it easier for you to collaborate with other Python programmers.

  3. The use of a global variable to store persistent data makes code hard to debug and test. When you have persistent data (like your Topics dictionary) together with a group of functions that operate on the data, it's usually best to organize this into a class.

  4. You have a global variable Topics but the things it contains are called "messages". It would be clearer if you stuck to one word to describe these things.

  5. Your implementation has the constraint that the only thing that can subscribe to a topic is an object with a method that has the same name as the topic. This is likely to be an annoying constraint. For suppose that I have a function f and I want to subscribe it to the topic named "t". Here's one way to do it:

    obj = type('_dummy', (object,), {})()
    obj.t = f
    subscribe(obj, 't')
    

    but this seems like the kind of boilerplate that a well-designed publish/subscribe library would not need.

  6. The implementation of publish_with_results seems dodgy to me. A topic is a set of subscribers (that is, the subscribers have no particular order), but publish_with_results returns a list of results (that is, in a particular order). So there is no way to match up the results with the subscribers.

    It's hard to say whether this is really a problem without knowing more about what you would use this for.

  7. Your implementation requires all topics to share the same namespace: they are all string keys in the Topics dictionary. But this means that if different modules want to use your publish/subscribe library, they need to coordinate their choices of names, to avoid clashes.

    It would be better to use Python's own namespace mechanism to handle name clashes, by making each topic into its own object, perhaps like this:

    class Topic(set):
        """A publish/subscribe topic, implemented as a set of functions.
    
        Subscribe by adding a function to the topic using the set
        methods. Send a message to the subscribers by calling the publish
        method.
    
        >>> t = Topic()
        >>> t.add(print)
        >>> t.publish('hello, world!')
        hello, world!
    
        """
    
        def publish(self, *args, **kwargs):
            """Publish message by calling all subscribers."""
            for s in self:
                s(*args, **kwargs)
    

    The example code in the docstring can be run using the doctest module.

2. Response to comments

  1. "You say it's harder to debug or test without a class - why?"

    When you're testing code you want to start each test case from a known state (not from whatever happens to be left over from the previous test cases). If you're testing code that's encapsulated in a class, then that's easy: you can just create a new instance of the class for each test case and throw it away when you're done. But if you're testing code that uses global variables, you have to explicitly reset all the global variables before starting the next test case.

  2. "Wouldn't each message ideally be unique anyway?"

    What I have in mind here is that a program may be made up of several modules or libraries that use the publish/subscribe library. For example, there may be a user interface toolkit that publishes a message when a user presses a button, and there may be a networking module that publishes a message when a networking event takes place.

    This would go wrong if these two modules happen to choose the same name for two different events. For example, the user interface toolkit publishes the "login" message when the user presses the "Login" button, and the networking module publishes the "login" message when the user successfully signs in to a remote service. These message names clash and so their subscribers incorrectly see the messages for the other event.

    So in this situation there needs to be some coordination in the choice of message names. But these two modules might be written by different programmers who have no idea that they would be combined in the same program.

    It would be better not to put all the messages in the same namespace.

\$\endgroup\$
  • \$\begingroup\$ I really like this reply, thank you! Questions/comments: 3) You say it's harder to debug or test without a class - why? 5) Yes - as stated this is a 'bare-bones' version I posted as I mostly wanted to focus on the lack of an Event object. I've other versions that get away from the need to name a message after the method (b/c it IS annoying, truly). 7) Wouldn't each message ideally be unique anyway? Perhaps I've misread your concern or don't have the experience to understand why that points to a problem in the library and not the user. \$\endgroup\$ – Stick Aug 25 '14 at 20:47
  • \$\begingroup\$ See revised answer. \$\endgroup\$ – Gareth Rees Aug 25 '14 at 21:56
  • \$\begingroup\$ I should probably update my OP to explain my thoughts as to my process. I am basically trying to minimize the number of references to the objects being messaged/subscribed/whatever, and so to that end the module as I posted it isn't really a very good example. StackExchange isn't fond of long discussions, but I'd love to be able to have a 'semi-extended' discussion about my goals with this little project. \$\endgroup\$ – Stick Aug 26 '14 at 16:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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