Python Observer Pattern: Examples, Tips?
However it does lack flexibility.Well... actually, this looks like a good design to me if an asynchronous API is what you want. It usually is. Maybe all you need is to switch from stderr to Python's
logging
module, which has a sort of publish/subscribe model of its own, what with Logger.addHandler()
and so on.If you do want to support observers, my advice is to keep it simple. You really only need a few lines of code.
class Event(object):
pass
class Observable(object):
def __init__(self):
self.callbacks = []
def subscribe(self, callback):
self.callbacks.append(callback)
def fire(self, **attrs):
e = Event()
e.source = self
for k, v in attrs.items():
setattr(e, k, v)
for fn in self.callbacks:
fn(e)
Your Job class can subclass Observable
. When something of interest happens, call self.fire(type="progress", percent=50)
or the like. alternate ways to implement observer pattern in python
There are many different ways you can "observe" something in python. Use property descriptors, custom __setattr__
, decorators...
Here is a simple example that uses first class functions:
class Foo(object):
def __init__(self):
self.observers = []
def register(self, fn):
self.observers.append(fn)
return fn # <-- See comments below answer
def notify_observers(self, *args, **kwargs):
for fn in self.observers:
fn(*args, **kwargs)
You can then register any callable.class Bar(object):
def do_something(self, *args, **kwargs):
pass # do something
foo = Foo()
bar = Bar()
foo.register(bar.do_something)
This will work properly. The call to do_something
will have the correct self
value. Because an object's methods are callable objects which carry a reference to the instance they are bound to.This might help understanding how it works under the hood:
>>> bar
<Bar object at 0x7f3fec4a5a58>
>>> bar.do_something
<bound method Bar.do_something of <Bar object at 0x7f3fec4a5a58>>
>>> type(bar.do_something)
<class 'method'>
>>> bar.do_something.__self__
<Bar object at 0x7f3fec4a5a58>
[edit: decorator example]
You may also use the register
method we defined above as a decorator, like this:
foo = Foo()
@foo.register
def do_something(*args, **kwargs):
pass # do something
For this to work, just remember that register
needs to return the callable it registered. Python Observer Design Pattern
There are already some nice sources of information about design patterns in Python.
Here's an interesting book on Python design patterns.
Last but not least...Alex Martelli gave a very interesting talk regarding this issue for Google Developer Day US.
Edit:
After reading that this is a duplicate of another StackOverflow question, I would recommend that everyone read that one as well. There are a lot of useful links and comments there.
Using decorators to implement Observer Pattern in Python3
To register the callback, you need to have an actual object. In your code, how is @Observable.register_observer
supposed to find which instance is should register on?
Please drop that Observable
thing that's a javaism, cumbersome in python.
Look at this.
#!/usr/bin/env python
class SomeData(object):
def __init__(self, value):
self.callbacks = []
self.foo = value
def register(self, callback):
self.callbacks.append(callback)
return callback
def notify(self, *args, **kwargs):
for callback in self.callbacks:
callback(self, *args, **kwargs)
class SomeGUI(object):
def redraw(self, obj, key, newvalue):
print('redrawing %s with value %s' % (self, newvalue))
if __name__ == '__main__':
my_data = SomeData(42)
# Register some function using decorator syntax
@my_data.register
def print_it(obj, key, value):
print('Key %s changed to %s' % (key, value))
# Register the SomeGUI element
my_gui = SomeGUI()
my_data.register(my_gui.redraw)
# Try changing it. Note my_data is dumb for now, notify manually.
my_data.foo = 10
my_data.notify("foo", 10)
I intentionally removed automatic notifications to illustrate registration by itself.Let's add it back. But there is no point using that Observable class. Let's make it lighter, simply defining an event class.
#!/usr/bin/env python3
class Event(object):
def __init__(self):
self.callbacks = []
def notify(self, *args, **kwargs):
for callback in self.callbacks:
callback(*args, **kwargs)
def register(self, callback):
self.callbacks.append(callback)
return callback
class SomeData(object):
def __init__(self, foo):
self.changed = Event()
self._foo = foo
@property
def foo(self):
return self._foo
@foo.setter
def foo(self, value):
self._foo = value
self.changed.notify(self, 'foo', value)
class SomeGUI(object):
def redraw(self, obj, key, newvalue):
print('redrawing %s with value %s' % (self, newvalue))
if __name__ == '__main__':
my_data = SomeData(42)
# Register some function using decorator syntax
@my_data.changed.register
def print_it(obj, key, value):
print('Key %s changed to %s' % (key, value))
# Register the SomeGUI element
my_gui = SomeGUI()
my_data.changed.register(my_gui.redraw)
# Try changing it.
my_data.foo = 10
As you probably noted now, the decorator syntax is useful in those circumstances:- You have a single registry. Either a singleton or the class itself class are first-order objects, and most are singletons.
- You dynamically define the function and register it as you go.
#!/usr/bin/env python3
class Event(object):
def __init__(self):
self.callbacks = []
def notify(self, *args, **kwargs):
for callback in self.callbacks:
callback(*args, **kwargs)
def register(self, callback):
self.callbacks.append(callback)
return callback
@classmethod
def watched_property(cls, event_name, key):
actual_key = '_%s' % key
def getter(obj):
return getattr(obj, actual_key)
def setter(obj, value):
event = getattr(obj, event_name)
setattr(obj, actual_key, value)
event.notify(obj, key, value)
return property(fget=getter, fset=setter)
class SomeData(object):
foo = Event.watched_property('changed', 'foo')
def __init__(self, foo):
self.changed = Event()
self.foo = foo
class SomeGUI(object):
def redraw(self, obj, key, newvalue):
print('redrawing %s with value %s' % (self, newvalue))
if __name__ == '__main__':
my_data = SomeData(42)
# Register some function using decorator syntax
@my_data.changed.register
def print_it(obj, key, value):
print('Key %s changed to %s' % (key, value))
# Register the SomeGUI element
my_gui = SomeGUI()
my_data.changed.register(my_gui.redraw)
# Try changing it.
my_data.foo = 10
For reference, all three programs output the exact same thing:$ python3 test.py
Key foo changed to 10
redrawing <__main__.SomeGUI object at 0x7f9a90d55fd0> with value 10
Python Observer Pattern: Examples, Tips?
However it does lack flexibility.Well... actually, this looks like a good design to me if an asynchronous API is what you want. It usually is. Maybe all you need is to switch from stderr to Python's
logging
module, which has a sort of publish/subscribe model of its own, what with Logger.addHandler()
and so on.If you do want to support observers, my advice is to keep it simple. You really only need a few lines of code.
class Event(object):
pass
class Observable(object):
def __init__(self):
self.callbacks = []
def subscribe(self, callback):
self.callbacks.append(callback)
def fire(self, **attrs):
e = Event()
e.source = self
for k, v in attrs.items():
setattr(e, k, v)
for fn in self.callbacks:
fn(e)
Your Job class can subclass Observable
. When something of interest happens, call self.fire(type="progress", percent=50)
or the like.
Related Topics
Most Efficient Way to Reverse a Numpy Array
Differencebetween Np.Array() and Np.Asarray()
How to Skip Iterations in a Loop
Representing and Solving a Maze Given an Image
Filename and Line Number of Python Script
Setting Up S3 for Logs in Airflow
Why Use Sys.Path.Append(Path) Instead of Sys.Path.Insert(1, Path)
How to Convert a List into a String with Spaces in Python
Django/Python Beginner: Error When Executing Python Manage.Py Syncdb - Psycopg2 Not Found
How to Create Module-Wide Variables in Python
Generate Rfc 3339 Timestamp in Python
Slicing of a Numpy 2D Array, or How to Extract an Mxm Submatrix from an Nxn Array (N>M)
Range Over Character in Python
Does Conda Replace the Need for Virtualenv
Find the Date for the First Monday After a Given Date
Is There a Builtin Identity Function in Python