开发者

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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜