Welcome, guest | Sign In | My Account | Store | Cart
5

The memento pattern is great for transaction-like processing. Having a handy implementation around might not be the worst thing.

Python, 81 lines
 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

3 comments

Oran Looney 6 years, 10 months ago  # | flag

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:

def transactional(method):
    def wrappedMethod(self, *args, **kwargs):
        state = Memento(self)
        try:
            return self.method(*args, **kwargs)
        except:
            state()
            raise

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:

def Memento(obj, deep=False):
   # state = (copy.copy, copy.deepcopy)[bool(deep)](obj.__dict__)
   state = (copy.copy if deep else copy.deepcopy)(obj.__dict__)
   def Restore():
     # obj.__dict__.clear()
     # obj.__dict__.update(state)
       obj.__dict__ = state
   return Restore

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.

Zoran Isailovski (author) 6 years, 5 months ago  # | flag

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.

Add a comment

Sign in to comment