ASPN ActiveState Programmer Network  
ActiveState, a division of Sophos
/ Home / Perl / PHP / Python / Tcl / XSLT /
/ Safari / My ASPN /
Cookbooks | Documentation | Mailing Lists | Modules | News Feeds | Products | User Groups
Submit Recipe
My Recipes

All Recipes
All Cookbooks


View by Category

Title: Transactionable Objects
Submitter: Simon Wittber (other recipes)
Last Updated: 2004/05/18
Version no: 1.0
Category: OOP

 

4 stars 5 vote(s)


Description:

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

Source: Text Source

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.



Add comment

Number of comments: 8

shallow copy, Dan Perl, 2004/08/29
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

Add comment

shallow copy, Dan Perl, 2004/08/29
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

Add comment

my bad, Dan Perl, 2004/08/29
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!
Add comment

back with a different example, Dan Perl, 2004/08/29
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.
Add comment

Thanks for the feedback, S W, 2004/09/14
Thanks for the criticism Dan, I'll make those changes now.
Add comment

DOH!, S W, 2004/09/14
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.
Add comment

honor __getstate__ and __setstate__, Harald Hoyer, 2008/03/22
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)

Add comment

Submitted an extended version, Harald Hoyer, 2008/03/22
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
Add comment



Highest rated recipes:

1. A simple XML-RPC server

2. Web service accessible ...

3. IPy Notify

4. Treat the Win32 Registry ...

5. a friendly mkdir()

6. Wrapping template engine ...

7. Assignment in expression

8. Changing return value ...

9. Implementation of sets ...

10. bag collection class




Privacy Policy | Email Opt-out | Feedback | Syndication
© 2006 ActiveState Software Inc. All rights reserved.