ActiveState Code

Recipe 284677: Transactionable Objects


This simple class allows sub-classes to commit changes to an instance to a history, and rollback to previous states.

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Transaction(object):
    def __init__(self):
        self.__log = []
    def _commit(self):
        self.__log.append(self.__dict__.copy())
    def _rollback(self):
        try:
            self.__dict__.update(self.__log.pop(-1))
        except IndexError:
            pass
            

Discussion

This class could be useful when a programmer needs to implement an undo history for objects which can be interactivly modified by the end-user.

Comments

  1. 1. At 2:56 p.m. on 29 aug 2004, Dan Perl said:

    shallow copy. Unfortunately, dict.copy makes a shallow copy. So this Transaction class will fail in all kinds of cases. Here is an example:

    class TryOut(Transaction):
        def __init__(self):
            self.bnd1 = (1,2,3)
            self.bnd2=[self.bnd1]
            Transaction.__init__(self)
            self._commit()
    
    t = TryOut()
    t.bnd2=None
    t._rollback()
    print t.bnd1, t.bnd2
    t.bnd1=None
    t.bnd2=None
    t._rollback()
    print t.bnd1, t.bnd2
    
  2. 2. At 3 p.m. on 29 aug 2004, Dan Perl said:

    shallow copy. I clicked on 'Add' too early. I wanted to explain more, so here I go again.

    Unfortunately, dict.copy makes a shallow copy. So this Transaction class will fail in all kinds of cases. Here is an example:

    class TryOut(Transaction):
        def __init__(self):
            self.bnd1 = (1,2,3)
            self.bnd2=[self.bnd1]
            Transaction.__init__(self)
            self._commit()
    
    t = TryOut()
    t.bnd2=None
    t._rollback()   # This works
    print t.bnd1, t.bnd2
    t.bnd1=None
    t.bnd2=None
    t._rollback()   # This doesn't work
    print t.bnd1, t.bnd2
    

    The output from that:

    (1, 2, 3) [(1, 2, 3)]
    None None
    
  3. 3. At 4:39 p.m. on 29 aug 2004, Dan Perl said:

    my bad. I forgot to commit the transactions, that's why it was not working. I'm still trying to figure out if the shallow dict.copy can somehow cause a problem, but until then, I'm changing my opinion. Good recipe!

  4. 4. At 6:37 p.m. on 29 aug 2004, Dan Perl said:

    back with a different example. Simon, I was wrong earlier and I strongly apologize. This recipe looked great to me the first time I looked at it, it's so simple and yet so powerful. On the other hand, instinctively I felt that there must be something wrong here with the shallow copy and that is why I've kept trying to find a hole in your recipe.

    I think I've got it now. Here is an example:

    class TryOut(Transaction):
        def __init__(self):
            self.myList=[1,2]
            Transaction.__init__(self)
    
    t=TryOut()
    t._commit()
    print t.myList,
    t.myList.append(3)
    t._rollback()
    print t.myList
    

    And the output is:

    [1, 2] [1, 2, 3]
    

    But now, here is a solution for the problem. Import the copy module and do a copy.deepcopy(self.__dict__) instead of the self.__dict__.copy(). I owed you that after the mistakes I've made.

  5. 5. At 1:47 a.m. on 14 sep 2004, S W said:

    Thanks for the feedback. Thanks for the criticism Dan, I'll make those changes now.

  6. 6. At 1:52 a.m. on 14 sep 2004, S W said:

    DOH! It seems I have lost control of this recipe...

    'No recipes found in the Cookbook that belong to you. Please select a cookbook to add a recipe.'

    ...and cannot make any changes.

  7. 7. At 6:59 a.m. on 22 mar 2008, Harald Hoyer said:

    honor __getstate__ and __setstate__. An extended version, which honors __getstate__ and __setstate__ could look like this.

    Also, I would self.__dict__.clear() before the update.

    class _Transaction(object):
        def __init__(self, *args):
            self.__log = []
    
        def _commit(self):
            if hasattr(self, '__getstate__'):
                state = getattr(self, '__getstate__')()
            else:
                state = copy.deepcopy(self.__dict__)
            self.__log.append(state)
    
        def _rollback(self):
            try:
                state = self.__log.pop(-1)
            except IndexError:
                pass
            else:
                if hasattr(self, '__setstate__'):
                    getattr(self, '__setstate__')(state)
                else:
                    self.__dict__.clear()
                    self.__dict__.update(state)
    
  8. 8. At 5:28 p.m. on 22 mar 2008, Harald Hoyer said:

    Submitted an extended version. See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/551788 for an extended version for this.

    The story of the evolution from this class can be read at http://www.harald-hoyer.de/linux/pythontransactionclass

Sign in to comment