Undo is a pretty common application functionality. I present a Python implementation where a History object is used to perform any action that may be undone. This object can go back and forth in the actions history. The idea is that, for every action, you provide one way to perform the action and one way to revert the action.


The History class definition follows. An instance maintains a list of actions, each action being composed of a way to perform something and a way to revert it:

[download history.py]

class Action(object):
    """Describes an action, and a way to revert that action"""
    def __init__(self, do, undo):
        """Both do and undo are in the form (function, [arg1, arg2, ...])."""
        self._do = do
        self._undo = undo
    def do(self):
        fun, args = self._do
        return fun(*args)
    def undo(self):
        fun, args = self._undo
        return fun(*args)
class History(object):
    "Maintains a list of actions that can be undone and redone."
    def __init__(self):
        self._actions = []
        self._last = -1
    def _push(self, action):
        if self._last < len(self._actions) - 1:
            # erase previously undone actions
            del self._actions[self._last + 1:]
        self._last = self._last + 1
    def undo(self):
        if self._last < 0:
            return None
            action = self._actions[self._last]
            self._last = self._last - 1
            return action.undo()
    def redo(self):
        if self._last == len(self._actions) - 1:
            return None
            self._last = self._last + 1
            action = self._actions[self._last]
            return action.do()
    def add(self, do, undo):
        """Does an action and adds it to history.
        Both do and undo are in the form (function, [arg1, arg2, ...]).
        action = Action(do, undo)
        return action.do()


Download the history module and place it in your working directory to try out the examples.

First, a History instance must be created.

from history import History
h = History()

You can then add an action to the history…

h.add(perform, revert)

… and revert or redo that action later:


The add() method accepts as arguments two functions and their corresponding list of arguments:

def foo(x, y):
def unfoo(z):
perform = (foo, [arg1, arg2])
revert = (unfoo, [arg3])
h.add(perform, revert)

I take as an example the modification of a dictionary d:

import history
d = {"x": 0, "y": 0}
h = history.History()
def modify(d, key, value):
    d[key] = value
def reset(d, saved):
saved = d.copy()
perform = (modify, [d, "x", 9999])
revert = (reset, [d, saved])
print "Original:  ", d
h.add(perform, revert)
print "Modified:  ", d
print "After undo:", d
print "After redo:", d
Original:   {'y': 0, 'x': 0}
Modified:   {'y': 0, 'x': 9999}
After undo: {'y': 0, 'x': 0}
After redo: {'y': 0, 'x': 9999}

Advanced Example

In the code below, undo/redo is provided to a Calculator class through a Python decorator.

from history import History
class Calculator(object):
    def __init__(self):
	self.value = 0.
        self._history = History()
        self.undo = self._history.undo
        self.redo = self._history.redo
    def _operation(method):
        def decorated(self, n):
            saved = self.value
            perform = (method, (self, n))
            revert = (self._set_value, (saved,))
            self._history.add(perform, revert)
        return decorated
    def _set_value(self, n):
        self.value = n
    def add(self, n):
	self.value += n
    def sub(self, n):
	self.value -= n
    def mult(self, n):
        self.value *= n
    def div(self, n):
	self.value /= n
calculator = Calculator()
print "Initial value:", calculator.value
print "+ 2 =", calculator.value
print "- 3 =", calculator.value
print "* 5 =", calculator.value
print "/ -2 =", calculator.value
for _ in range(4):
    print "undo:", calculator.value
for _ in range(4):
    print "redo:", calculator.value


Initial value: 0.0
+ 2 = 2.0
- 3 = -1.0
* 5 = -5.0
/ -2 = 2.5

undo: -5.0
undo: -1.0
undo: 2.0
undo: 0.0

redo: 2.0
redo: -1.0
redo: -5.0
redo: 2.5

Posted by pierre on 16 December 2010 in python

Leave a Reply