Easy Python Patterns: The Memento pattern, or how to build an undo stack

Introduction

You can use the memento pattern to (partially) expose the internal state of an object. One use case for this pattern is to serialize an object to a file or as JSON for example, another is to build an undo stack.

One bit of advice when using this pattern is only expose the state that you need to expose, to comply with the Least Privilege Principle.

It looks like this:

This pattern usually consist of three parts:

  1. The Originator, this is the object whose internal gets exposed.
  2. The Client, this is the object that wants to change the state of the Originator.
  3. Before the state changes, we instantiate Memento-object with the original state, and stored. If you want to build an undo-system, these Memento objects could be pushed onto some stack.

In an empty directory, open your IDE, and a file called ‘memento.py’:

In our sample project we will built a simple multi-level undo mechanism.

Implementation in Python

We will start by defining the Memento class:

class Memento[T]:
    _state: T = None

    def __init__(self, initial_state: T):
        self._state = initial_state

    @property
    def state(self) -> T:
        return self._state

    @state.setter
    def state(self, value: T):
        self._state = value

A few notes:

  • This is a generic, it will hold values of any type
  • Note the use of @property and the @*.setter to denote our getter and setter methods

The Originator class

The Originator class is the class whose state we want to capture:

class Originator[T]:
    _states: list[Memento(T)] = None

    def __init__(self, initial_state: Memento(T)):
        self._states = [initial_state]

    @property
    def state(self) -> Memento(T):
        if self._states:
            return self._states[-1].state
        else:
            return None

    @state.setter
    def state(self, new_state: Memento(T)):
        self._states.append(new_state)

    def restore_state(self) -> Memento(T):
        if self._states:
            self._states.pop()
        else:
            return None

A method by method explanation:

The constructor

In the constructor we construct a list of Memento objects. The list gets one initial element, namely a Memento with the state passed to the constructor

The state property

Like in the Memento class we use the @property syntax to access and modify our state

Undoing our actions in the restore_state() method

Like in the get_state() method, we check if we have states, if so, perform a pop() operation removing the last one and returning it. If the states list is empty, we return a None.

The Client class

In the Client class we talk to the Originator class:

class Client[T]:
    _originator: Originator(T) = None

    def __init__(self, initial_state: T):
        self._originator = Originator(Memento(initial_state))

    @property
    def state(self) -> T:
        return self._originator.state

    @state.setter
    def state(self, new_state: T):
        self._originator.state = Memento(new_state)

    def restore(self) -> Memento(T):
        return self._originator.restore_state()

Method by method:

The constructor

In the constructor, we simply initialize the _originator field with a freshly-constructed Orginator, passing it the initial state which passed to the constructor

The state Property

Like in our previous classes we use the @property syntax for access and modifying the state.

Restoring our actions in restore()

Also a wrapper for the corresponding method in the Originator class.

Testing it

Time to test our code:

if __name__ == "__main__":
    client: Client(str) = Client("on")
    client.state = "off"
    client.state = "standby"
    print(client.state)
    client.restore()
    print(client.state)
    client.restore()
    print(client.state)

A short description:

  1. We initialize an object of the Client class with an initial state of ‘on’
  2. After that we change the state twice.
  3. We print the current state, which should be ‘standby’
  4. Next we restore, or undo our set_state() call, and we print out off
  5. Repeat this and we should see ‘on’

Conclusion

In this example I chose a very simplistic example of implementing some sort of undo stack, but even from that you can see how flexible and useful this pattern can be.

One possible extension would be to make it threadsafe, that is if the state of the Originator class could be changed from different threads. This would be an interesting assignment for one of my next articles.

Leave a Reply

Your email address will not be published. Required fields are marked *