patternpythonMinor
Light Python pubsub lib, but is it too light?
Viewed 0 times
libbuttoolightpythonpubsub
Problem
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.
There's no
Objects can of course be subscribed to events outside their
This is actually an early version; I've a few different implementations - one t
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 t
Solution
- 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.
-
There's no top-level documentation. How am I supposed to use this code? You could provide a module-level docstring.
-
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.-
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.-
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.-
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.
-
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.
-
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.- Response to comments
-
"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.
-
"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.
Code Snippets
obj = type('_dummy', (object,), {})()
obj.t = f
subscribe(obj, 't')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)Context
StackExchange Code Review Q#60633, answer score: 4
Revisions (0)
No revisions yet.