|
|
 |
|
Title: Transactionable Objects
Submitter: Simon Wittber
(other recipes)
Last Updated: 2004/05/18
Version no: 1.0
Category:
OOP
|
|
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
|
|
|
|
|
 |
|