The memento pattern is great for transaction-like processing. Having a handy implementation around might not be the worst thing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | import copy
def Memento(obj, deep=False):
state = (copy.copy, copy.deepcopy)[bool(deep)](obj.__dict__)
def Restore():
obj.__dict__.clear()
obj.__dict__.update(state)
return Restore
class Transaction:
"""A transaction guard. This is realy just
syntactic suggar arount a memento closure.
"""
deep = False
def __init__(self, *targets):
self.targets = targets
self.Commit()
def Commit(self):
self.states = [Memento(target, self.deep) for target in self.targets]
def Rollback(self):
for state in self.states:
state()
class transactional(object):
"""Adds transactional semantics to methods. Methods decorated
with @transactional will rollback to entry state upon exceptions.
"""
def __init__(self, method):
self.method = method
def __get__(self, obj, T):
def transaction(*args, **kwargs):
state = Memento(obj)
try:
return self.method(obj, *args, **kwargs)
except:
state()
raise
return transaction
if __name__ == '__main__':
class NumObj(object):
def __init__(self, value):
self.value = value
def __repr__(self):
return '<%s: %r>' % (self.__class__.__name__, self.value)
def Increment(self):
self.value += 1
@transactional
def DoStuff(self):
self.value = '1111' # <- invalid value
self.Increment() # <- will fail and rollback
print
n = NumObj(-1)
print n
t = Transaction(n)
try:
for i in range(3):
n.Increment()
print n
t.Commit()
print '-- commited'
for i in range(3):
n.Increment()
print n
n.value += 'x' # will fail
print n
except:
t.Rollback()
print '-- rolled back'
print n
print '-- now doing stuff ...'
try:
n.DoStuff()
except:
print '-> doing stuff failed!'
import traceback
traceback.print_exc(0)
pass
print n
|
Closures offer truly charming solution opportunities. In this example, the Memento function returns a closure that keeps the originator as well as the captured state in its scope, and restores the originator's state when called. Now, according to the memento pattern, the object representing the originator's state should be opaque, so the only thing you should be able to do with it is return it to the originator for state restoration. Well, I think the presented closure is as close (rem: note that subtle wording ;) to this definition as it can be.
Okay, we knew this was going to be easy with python. (After all, it seems to be the perfect design pattern implementation language, as you can almost always find a way to implement a pattern in a generic and reusable way.) And, once we've got it, we can build on top of it all sorts of useful (or at least nice-sounding ;) things:
- Transaction guards that work with multiple objects
- Decorators to add transactional action semantics to methods (a transactional action either entirely succeeds or fails w/o changing system state)
- Undo/Redo semantics (by swapping before/after states)
- ...
Cheers and happy transacting!
[See Also] Memento pattern description at http://en.wikipedia.org/wiki/Memento_pattern
Wrapping with a descriptor? Great recipe. Todd Proebsting mentions built-in local transaction support as a possible direction for future languages:
http://research.microsoft.com/~toddpro/papers/disruptive.ppt
I see you're using a descriptor class for the 'transactional' decorator. The following alternative:
seems a little shorter and clearer. It seems to work, at least for the provided test case. Is there a design constraint I'm missing? Perhaps it needs to be read-only?
I also have some style suggestions for Memento:
The first suggestion is simply an update to the new 2.5 syntax.
The second releases the orignal __dict__ for garbage collection and changes obj.__dict__ directly into a reference to state. state is part of the closure and will not have any other references.
Again, great recipe; it really came in handy.
Thx Oran. (and sorry for my VERY late response)
Obviously, you're a fan of closures - like me ;)
Concerning your suggestion to switch to the new 2.5 syntax: I'm rather conservative on this. I like recipes that are reasonably backward compatible, so people who are stuck to older python versions for legacy reasons can benefit, too. However, your suggestion is a nice hint for those NOT stuck to pre-2.5 versions. Thanks again.
See also. http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/551788