Maintaining a roll-backable flow of code in python without extreme identation
I've encountered a situation where I'm working over a piece of code where I command changes on a remote object (that is one I can't duplicate to work over a clone), then ask the remote object for some operation in the new state and revert all the changes I made to it by a sequence of opposite commands. The problem is that if in the middle of all these changes I encounter an error, I want to be able to roll-back all the changes I made so far.
The best fitting solution that came to my mind is the python try-finally workflow, but it's rather problematic when the sequence of commands is long:
try:
# perform action
try:
# perform action
try:
# ...
finally:
# unroll
finally:
# unroll
finally:
# unroll
This way, the more commands I need the deeper my indentation and nesting goes and the less my code is readable. I've considered some other solutions such as maintaining a stack where for every command I push a rollback action, but this could get rather complicated, and I dislike pushing bound methods into stacks. I've also considered incrementing a counter for every action I perform then in a single finally decide on the kind of rollback I want depending on the counter, but again, the maintainability of such code becomes a pain.
Most hits I got on searches for "transactions" and "rollback" were DB relat开发者_如何学Pythoned and didn't fit very well to a more generic kind of code... Anyone has a good idea as to how to systematically flatten this atrocity?
Wouldn't Context Manager objects and the with statement improve the situation? Especially if you can use a version of Python where the with statement supports multiple context expressions, as 2.7 or 3.x. Here's an example:
class Action(object):
def __init__(self, count):
self.count = count
def perform(self):
print "perform " + str(self.count)
if self.count == 2:
raise Exception("self.count is " + str(self.count))
def commit(self):
print "commit " + str(self.count)
def rollback(self):
print "rollback " + str(self.count)
def __enter__(self):
perform()
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_value is None:
self.commit()
else:
self.rollback()
with Action(1), Action(2), Action(3):
pass
You'd have to move your code to a set of "transactional" classes, such as Action
above, where the action to be performed is executed in the __enter__()
method and, if this terminates normally, you would be guaranteed that the corresponding __exit()__
method would be called.
Note that my example doesn't correspond exactly to yours; you'd have to tune what to execute in the __enter__()
methods and what to execute in the with
statement's body. In that case you might want to use the following syntax:
with Action(1) as a1, Action(2) as a2:
pass
To be able to access the Action
objects from within the body of the with
statement.
精彩评论