使用Python引发异常时回滚操作的最佳方法

Best way to roll back actions when an exception is raised with Python

我有一个这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# step 1 remove from switch    
for server in server_list:
    remove_server_from_switch(server)
    logger.info("OK : Removed %s", server)

# step 2 remove port
for port in port_list:
    remove_ports_from_switch(port)
    logger.info("OK : Removed port %s", port)

# step 3 execute the other operations
for descr in pairs:
    move_descr(descr)

# step 4 add server back to switch    
for server in server_list:
    add_server_to_switch(server)
    logger.info("OK : server added %s", server)

# step 5 add back port
for port in port_list:
    add_ports_to_switch(port)
    logger.info("OK : Added port %s", port)

for循环中的函数可以引发异常,或者用户可以使用ctrl+c中断脚本。但是,如果在执行过程中引发异常,我希望通过撤消之前已经完成的更改来进入回滚模式。我的意思是,如果在步骤3中出现异常,我必须回滚步骤1和2(通过执行步骤4和5中的操作)。或者,如果用户试图在步骤1的for循环中间使用ctrl+c停止脚本,我希望回滚该操作并添加已删除的服务器。

除了使用例外情况,如何才能用一种好的Python式的方式来做呢?:)


这就是上下文管理器的作用。详细阅读WITH语句,但总的来说,您需要编写上下文管理器类,其中__enter____exit__函数用于删除/重新添加服务器/端口。然后您的代码结构就变成了:

1
2
3
4
with RemoveServers(server_list):
    with RemovePorts(port_list):
        do_stuff
# exiting the with blocks will undo the actions

也许这样的方法会奏效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
undo_dict = {remove_server_from_switch: add_server_to_switch,
             remove_ports_from_switch: add_ports_to_switch,
             add_server_to_switch: remove_server_from_switch,
             add_ports_to_switch: remove_ports_from_switch}

def undo_action(action):
    args = action[1:]
    func = action[0]
    undo_dict[func](*args)

try:

    #keep track of all successfully executed actions
    action_list = []

    # step 1 remove from switch    
    for server in server_list:
        remove_server_from_switch(server)
        logger.info("OK : Removed %s", server)
        action_list.append((remove_server_from_switch, server))

    # step 2 remove port
    for port in port_list:
        remove_ports_from_switch(port)
        logger.info("OK : Removed port %s", port)
        action_list.append((remove_ports_from_switch, port))

    # step 3 execute the other operations
    for descr in pairs:
        move_descr(descr)

    # step 4 add server back to switch    
    for server in server_list:
        add_server_to_switch(server)
        logger.info("OK : server added %s", server)
        action_list.append((add_server_to_switch, server))

    # step 5 add back port
    for port in port_list:
        add_ports_to_switch(port)
        logger.info("OK : Added port %s", port)
        action_list.append((add_ports_to_switch, port))

except Exception:
    for action in reverse(action_list):
        undo_action(action)
        logger.info("ERROR Recovery : undoing {func}({args})",func = action[0], args = action[1:])

finally:
    del action_list

编辑:正如Tzaman下面所说,在这种情况下,最好的做法是将整个事情包装成一个上下文管理器,并使用with语句。然后,不管是否遇到错误,您的所有操作在with块结束时都将被撤消。

下面是它可能的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ActionManager():
    def __init__(self, undo_dict):
        self.action_list = []
        self.undo_dict = undo_dict
    def action_pop(self):
        yield self.action_list.pop()
    def action_add(self, *args):
        self.action_list.append(args)
    def undo_action(self, action):
        args = action[1:]
        func = action[0]
        self.undo_dict[func](*args)
    def __enter__(self):
        return self
    def __exit__(self, type, value, traceback):
        for action in self.action_stack:
            undo_action(action)
            logger.info("Action Manager Cleanup : undoing {func}({args})",func = action[0], args = action[1:])

现在你可以这样做了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#same undo_dict as before
with ActionManager(undo_dict) as am:

    # step 1 remove from switch    
    for server in server_list:
        remove_server_from_switch(server)
        logger.info("OK : Removed %s", server)
        am.action_add(remove_server_from_switch, server)

    # step 2 remove port
    for port in port_list:
        remove_ports_from_switch(port)
        logger.info("OK : Removed port %s", port)
        am.action_add(remove_ports_from_switch, port)

    # step 3 execute the other operations
    for descr in pairs:
        move_descr(descr)

    # steps 4 and 5 occur automatically

另一种方法是在__enter__方法中添加服务器/端口,这可能会更好。您可以将上面的ActionManager子类化,并在其中添加端口添加和删除逻辑。

__enter__方法甚至不必返回ActionManager类的一个实例——如果这样做是有意义的,甚至可以编写它,以便with SwitchManager(servers,ports)返回pairs对象,最后可以这样做:

1
2
3
with SwitchManager(servers, ports) as pairs:
    for descr in pairs:
        move_descr(descr)