Hi, today I’m going to introduce you to a new design pattern called the Memento pattern.

Memento is a memento in English, in this case, a deep copy of an object. The function of transaction is realized by deep copy of object. Those who have learned about the database should know that some operations in the database are bound, either successfully executed together, or not executed together, absolutely do not run some operations executed, some operations are not executed. This point is called a transaction.

Deep copy

Let’s start with a quick review of copying in Python.

Copying has functions in many languages, and Python is no exception. There are two copy functions in Python, one is copy and the other is deepcopy. Also known as deep copy and shallow copy, the difference between the two is very simple, in short, the shallow copy will only copy the parent object, does not copy the child object of the parent object.

For example, in the figure below, b is a shallow copy of A. We can see that when 5 is inserted into A [2], b also has an extra 5. Because they store the same reference with subscript 2, the same change occurs in B when a is inserted. We can also see that when we change a[0], there is no corresponding change in B. Because a[0] is a number, a number is a value stored directly by the underlying type, not a reference.

The corresponding to the shallow copy is the deep copy. It can be seen that when elements are inserted into a[2], the deep copy b does not have corresponding changes.

memento

With copy, we can implement the memento function, which makes a backup of the object. In Python, for an object called obj, all its members and functions are stored in the dict called obj.__dict__. That is, if we copy an object’s __dict__, we are actually making a copy of the object.

We can easily implement memento functions by using copy, so let’s look at the code first.

from copy import copy, deepcopy

def memento(obj, deep=False) :
    state = deepcopy(obj.__dict__) if deep else copy(obj.__dict__)

    def restore() :
        obj.__dict__.clear()
        obj.__dict__.update(state)
    
    return restore
Copy the code

Memento is a higher-order function that returns an execution function rather than a concrete execution result. If you’re not familiar with higher-order functions, take a look back at higher-order functions in Python.

The logic is not hard to understand. The parameters passed in are an obj object and a bool flag. Flag indicates deep copy or shallow copy, and OBj is the object that we need to snapshot or archive. We want to restore the contents of the object frame unchanged, so the scope of our copy is clear, which is obj.__dict__, which stores all the key information about the object.

Take a look at the function restore. The content is simple, just two lines. The first line is to clear the contents of OBJ’s current __dict__, and the second step is to restore it with the previously saved state. In fact, restore performs a rollback of OBj, so let’s take a look at the process. When we run the memento function, we get restore. When we run this function, the contents of obj are rolled back to the state when memento was last executed.

Once we understand the logic in Memento, we are not far away from implementing transactions. There are two ways to implement transactions, one through objects and one through decorators, so let’s take them one at a time.

The Transaction object

Object-oriented implementation is relatively simple, and it is similar to the process we usually use transactions. Transaction objects should provide two functions, one for commit and one for rollback. That is, when we execute successfully, we commit and take a snapshot of the results. Rollback if the execution fails, rollback the result of the object to the state at the last commit.

Once we understand the memento function, we see that commit and ROLLBACK correspond to executing the memento function and executing the restore function. This makes it easy to write code:

class Transaction:

    deep = False
    states = []

    def __init__(self, deep, *targets) :
        self.deep = deep
        self.targets = targets
        self.commit()

    def commit(self) :
        self.states = [memento(target, self.deep) for target in self.targets]

    def rollback(self) :
        for a_state in self.states:
            a_state()
Copy the code

Since we may need more than one transaction object, targets is designed as an array.

The Transaction decorator

We can also implement transactions as decorators that can be used via annotations.

The code principle is the same, but the implementation logic is based on decorators. If you are familiar with decorators, it is not difficult to understand. Here args[0] is an instance of a class that we need to guarantee the body of the transaction.

from functools import wraps

def transactional(func) :
    @wraps(func)
    def wrapper(*args, **kwargs) :
        # args[0] is obj
        state = memento(args[0])
        try:
            func(*args, **kwargs)
        except Exception as e:
            state()
            raise e
    return wrapper
Copy the code

This is how we write decorators normally, but we can also use classes to implement decorators, but the principle is similar, with a few different details.

class Transactional:

    def __init__(self, method) :
        self.method = method

    def __get__(self, obj, cls) :
        def transaction(*args, **kwargs) :
            state = memento(obj)
            try:
                return self.method(*args, **kwargs)
            except Exception as e:
                state()
                raise e
        return transaction
Copy the code

When we apply this annotation to a class method, instead of fetching the Transactional class, we execute the __get__ method of the Transactional class when we execute obj. XXX. And we pass in obj and the corresponding type of obj, which is what obj and CLS mean here. This is a common way to implement decorators using classes. Let’s post some common code to compare and learn.

class Wrapper:
    def __init__(self, func) :
        wraps(func)(self)

    def __call__(self, *args, **kwargs) :
        return self.__wrapped__(*args, **kwargs)

    def __get__(self, instance, cls) :
        if instance is None:
            return self
        else:
            return types.MethodType(self, instance)
Copy the code

This is a case of using a class to implement the decorator. We can see that the __get__ function returns self, which returns the Wrapper class. A class is usually not directly executable, and to make it executable, a __call__ function is implemented. It doesn’t matter if you still don’t understand it, you can ignore this part. Class decorators are also uncommon, as long as we are familiar with the methods of higher-order functions.

In actual combat

Finally, let’s take a look at a practical example. We have implemented a NumObj class that is compatible with the use of the above two transactions.

class NumObj:
    def __init__(self, value) :
        self.value = value

    def __repr__(self) :
        return '<%s, %r>' % (self.__class__.__name__, self.value)

    def increment(self) :
        self.value += 1

    @transactional
    def do_stuff(self) :
        self.value += '111'
        self.increment()
        
        
if __name__ == '__main__':
    num_obj = NumObj(-1)

    a_transaction = Transaction(True, num_obj)
	# to use Transaction
    try:
        for i in range(3):
            num_obj.increment()
            print(num_obj)

        a_transaction.commit()
        print('----committed')
        for i in range(3):
            num_obj.increment()
            print(num_obj)
        num_obj.value += 'x'
        print(num_obj)
    except Exception:
        a_transaction.rollback()
        print('----rollback')

    print(num_obj)
	# use Transactional
    print('-- now doing stuff')
    num_obj.increment()

    try:
        num_obj.do_stuff()
    except Exception:
        print('-> doing stuff failed')
        import sys
        import traceback
        traceback.print_exc(file=sys.stdout)

    print(num_obj)
Copy the code

For Transaction, which is an object oriented implementation, we need to create an additional Transaction instance to control whether or not a rollback is performed in a try catch. Annotations are more flexible in that they are automatically rolled back without requiring much additional action.

In general, we prefer to use annotations because it’s cleaner, more Pythonic, and shows the power of Python. The first approach is a little more conventional, but has the advantage of being more readable and less difficult to implement. If you need to use it in the actual work, you can choose according to your actual situation, both of which are good methods.

That’s all for today’s article. I sincerely wish you all a fruitful day. If you still like today’s content, please join us in a three-way support.

Click on the link for more articles