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

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

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

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.

8 comments

Dan Perl 19 years, 7 months ago  # | flag

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
Dan Perl 19 years, 7 months ago  # | flag

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
Dan Perl 19 years, 7 months ago  # | flag

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!

Dan Perl 19 years, 7 months ago  # | flag

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.

S W 19 years, 6 months ago  # | flag

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

S W 19 years, 6 months ago  # | flag

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.

Harald Hoyer 16 years ago  # | flag

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)
Harald Hoyer 16 years ago  # | flag

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

Created by Simon Wittber on Tue, 18 May 2004 (PSF)
Python recipes (4591)
Simon Wittber's recipes (1)

Required Modules

  • (none specified)

Other Information and Tasks