patternpythonMinor
Observer pattern implementation without subclassing
Viewed 0 times
withoutobserversubclassingimplementationpattern
Problem
I have not yet seen an implementation of the observer pattern in Python which satisfies the following criteria:
Here is my attempt (now on github), which allows bound methods to observe other bound methods (see the test example below of example usage):
```
import weakref
import functools
class ObservableMethod(object):
"""
A proxy for a bound method which can be observed.
I behave like a bound method, but other bound methods can subscribe to be
called whenever I am called.
"""
def __init__(self, obj, func):
self.func = func
functools.update_wrapper(self, func)
self.objectWeakRef = weakref.ref(obj)
self.callbacks = {} #observing object ID -> weak ref, methodNames
def addObserver(self, boundMethod):
"""
Register a bound method to observe this ObservableMethod.
The observing method will be called whenever this ObservableMethod is
called, and with the same arguments and keyword arguments. If a
boundMethod has already been registered to as a callback, trying to add
it again does nothing. In other words, there is no way to sign up an
observer to be called back multiple times.
"""
obj = boundMethod.__self__
ID = id(obj)
if ID in self.ca
- A thing which is observed should not keep its observers alive if all other references to those observers disappear.
- Adding and removing observers should be pythonic. For example, if I have an object
foowith a bound method.bar, I should be able to add an observer by calling a method on the method, like this:foo.bar.addObserver(observer).
- I should be able to make any method observable without subclassing. For example, I should be able to make a method observable just by decorating it.
- This must work for types which are unhashable, and must be able to be used on an arbitrary number of methods per class.
- The implementation should be comprehensible to other developers.
Here is my attempt (now on github), which allows bound methods to observe other bound methods (see the test example below of example usage):
```
import weakref
import functools
class ObservableMethod(object):
"""
A proxy for a bound method which can be observed.
I behave like a bound method, but other bound methods can subscribe to be
called whenever I am called.
"""
def __init__(self, obj, func):
self.func = func
functools.update_wrapper(self, func)
self.objectWeakRef = weakref.ref(obj)
self.callbacks = {} #observing object ID -> weak ref, methodNames
def addObserver(self, boundMethod):
"""
Register a bound method to observe this ObservableMethod.
The observing method will be called whenever this ObservableMethod is
called, and with the same arguments and keyword arguments. If a
boundMethod has already been registered to as a callback, trying to add
it again does nothing. In other words, there is no way to sign up an
observer to be called back multiple times.
"""
obj = boundMethod.__self__
ID = id(obj)
if ID in self.ca
Solution
I like your code: its quite interesting. While reviewing, I was hoping to see if there was a better way if keeping track of the instances of each
With that being said, I have a few recommendations, mostly centered around naming:
-
Rename your
Currently
As for renaming
-
Improve your variable names.
Currently you have several 1-letter or 2-letter variable names. Make those names more descriptive:
Yes, in context we can deduce the meaning of the variable names. However, it pays to be more explicit rather than trusting that everyone understands what is going on:
In the example above is another point I want to make:
My final point about variable names deals with multiple word names. You used 'camelCase' in your code. Pythonic convention say that
-
Spacing
Insert blank lines to help group logical sections of code. Looking at your
-
As this link says, %-notation isn't becoming obsolete. However, it says that using
This also saves you from having to create temporary tuples just to print information.
Outside of these comments, the code looks neat and Pythonic.
ObservableMethod or if we could combine the ObservableMethod and ObservableMethodDescriptor classes. However, I could not think of a better way to store the instances of each ObservableMethod.With that being said, I have a few recommendations, mostly centered around naming:
-
Rename your
event function and Cleanup class.Currently
event is quite generic. Plus, it doesn't follow the 'function-names-start-with-verbs' convention. I would recommend renaming it to make_observable. This conveys much better what it (and its decorator) actually does.As for renaming
Cleanup: because of its verb-based name, it feels like a function. Classes are things, thus it makes sense to have a noun-based name; maybe CleanupHandler. -
Improve your variable names.
Currently you have several 1-letter or 2-letter variable names. Make those names more descriptive:
def __get__(self, inst, cls):
. . .
if ID in self.instances:
# World record, organic matter?
wr, om = self.instances[ID]Yes, in context we can deduce the meaning of the variable names. However, it pays to be more explicit rather than trusting that everyone understands what is going on:
def __get__(self, inst, cls):
. . .
if ID in self.instances:
# Much better.
weak_ref, observable_method = self.instances[ID]In the example above is another point I want to make:
ID is not constant (as convention says its name suggests). I would recommend renaming it to obj_id or something of the like. You could just use id but that may be a little confusing (and possibly dangerous) since id is a basic Python function.My final point about variable names deals with multiple word names. You used 'camelCase' in your code. Pythonic convention say that
underscores_in_names is preferred.-
Spacing
Insert blank lines to help group logical sections of code. Looking at your
__get__ code, inserting blank lines helps the visual flow of the method:def __get__(self, inst, cls):
if inst is None:
return self
ID = id(inst)
if ID in self.instances:
wr, om = self.instances[ID]
if not wr():
msg = "Object id %d should have been cleaned up"%(ID,)
raise RuntimeError(msg)
else:
wr = weakref.ref(inst, Cleanup(ID, self.instances))
om = ObservableMethod(inst, self._func)
self.instances[ID] = (wr, om)
return om-
format() vs. %-NotationAs this link says, %-notation isn't becoming obsolete. However, it says that using
format() is the preferred method, especially if you are concerned about future compatibility.# Your way
>>> print 'Hello %s!'%('Darin',)
# New way
>>> print 'Hello {}!'.format('Darin')This also saves you from having to create temporary tuples just to print information.
Outside of these comments, the code looks neat and Pythonic.
Code Snippets
def __get__(self, inst, cls):
. . .
if ID in self.instances:
# World record, organic matter?
wr, om = self.instances[ID]def __get__(self, inst, cls):
. . .
if ID in self.instances:
# Much better.
weak_ref, observable_method = self.instances[ID]def __get__(self, inst, cls):
if inst is None:
return self
ID = id(inst)
if ID in self.instances:
wr, om = self.instances[ID]
if not wr():
msg = "Object id %d should have been cleaned up"%(ID,)
raise RuntimeError(msg)
else:
wr = weakref.ref(inst, Cleanup(ID, self.instances))
om = ObservableMethod(inst, self._func)
self.instances[ID] = (wr, om)
return om# Your way
>>> print 'Hello %s!'%('Darin',)
# New way
>>> print 'Hello {}!'.format('Darin')Context
StackExchange Code Review Q#49782, answer score: 2
Revisions (0)
No revisions yet.