作者 | 梁唐 責編 | 張文
頭圖 | CSDN 下載自東方 IC
來源 | TechFlow(ID:techflow2019)
大家好,今天給大家介紹一個新的設計模式,叫做 memento 模式。
memento 在英文當中是紀念品的意思,在這裡,指的是對象的深度拷貝。通過對對象深度拷貝的方法來實現事務的功能。
有了解過資料庫的小夥伴應該都知道,在資料庫中有些操作是綁定的,要麼一起執行成功,要麼一起不執行,絕對不允許某些操作執行了,某些操作沒有執行的情況發生。這一點就被稱為事務。
深度拷貝
我們先來簡單回顧一下 Python 當中的拷貝。
拷貝在很多語言當中都有對應的函數,在 Python 當中也不例外。Python 中的拷貝函數有兩個,一個是 copy,另外一個是 deepcopy。也就是常說的深拷貝和淺拷貝,這兩者的區別也非常簡單,簡而言之就是淺拷貝只會拷貝父類對象,不會拷貝父類對象當中的子對象。
我們來看一個例子,在下圖當中 b 是 a 的淺拷貝,我們可以看到當 a[2] 當中插入了 5 之後,b 當中同樣也多了一個 5 。因為它們下標 2 存儲的是同一個引用,所以當 a 當中插入的時候,b 當中也發生了同樣的改變。我們也可以看到,當我們改變了 a[0] 的時候,b 當中則沒有發生對應的改變。因為a[0] 是一個數字,數字是基礎類型直接存儲的值而不是引用。
與淺拷貝對應的就是深拷貝,我們可以看到,當 a[2] 當中插入元素的時候,深度拷貝出來的 b 並不會發生對應的變化。
memento
利用拷貝,我們可以實現 memento 函數。它的作用是給對象做備份。在Python 當中,對於一個對象 obj 來說,它所有的成員以及函數等信息全是儲存在obj.__dict__這個 dict 當中的。也就是說如果我們將一個對象的__dict__拷貝一份的話,其實就相當於我們把對象拷貝了一份。
通過使用拷貝,我們可以很容易實現 memento 函數,先來看代碼:
from copy import copy, deepcopydefmemento(obj, deep=False): state = deepcopy(obj.__dict__) if deep else copy(obj.__dict__)defrestore(): obj.__dict__.clear() obj.__dict__.update(state)return restore
memento 是一個高階函數,它返回的結果是執行函數,而不是具體的執行結果。如果對高階函數不太熟悉的同學,可以去回顧一下 Python 當中高階函數的相關內容。
這裡面的邏輯不難理解,傳入的參數是一個 obj 的對象和一個 bool 型的flag。flag 表示使用深拷貝或淺拷貝,obj 就是我們需要做對應快照或者是存檔的對象。我們希望在對象框架不變的基礎上恢復其中的內容,所以我們拷貝的範圍很明確,就是 obj.__dict__,這當中存儲了對象的所有關鍵信息。
我們看下 restore 這個函數,當中的內容其實很簡單,只有兩行。
第一行是清空 obj 目前__dict__當中的內容,第二步是用之前保存的 state 來還原。其實 restore 執行的是一個回滾 obj 的功能,我們捋一下整個過程。
我們運行 memento 函數會得到 restore 這個函數,當我們執行這個函數的時候,obj 當中的內容會回滾到上次執行 memento 時的狀態。
理解了 memento 當中的邏輯之後,距離我們實現事務就不遠了。關於事務我們有兩種實現方法,一種是通過對象,一種是通過裝飾器,我們一個一個來說吧。
Transaction對象
面向對象實現的方式比較簡單,它和我們平時使用事務的過程也比較近似。Transaction 對象當中應該提供兩個函數,一個是 commit 一個是rollback。也就是說當我們執行成功之後我們執行 commit,對執行的結果進行快照。如果執行失敗則 rollback,將對象的結果回滾到上一次 commit時的狀態。
我們理解了 memento 函數之後,會發現 commit 和 rollback 剛好對應執行 memento 函數以及執行 restore 函數。這樣我們不難寫出代碼:
classTransaction: deep = False states = []def__init__(self, deep, *targets):self.deep = deepself.targets = targetsself.commit()defcommit(self):self.states = [memento(target, self.deep) for target inself.targets]defrollback(self):for a_state inself.states: a_state()
由於我們需要事務的對象可能不止一個,所以這裡的 targets 設計成了數組的形式。
Transaction裝飾器
我們也可以把事務實現成裝飾器,這樣我們可以通過註解的方式來使用。
這裡的代碼原理也是一樣的,只不過實現邏輯基於裝飾器而已。如果對裝飾器熟悉的同學,其實也不難理解。這裡的 args[0] 其實就是某一個類的實例,也就是我們需要保證事務的主體。
from functools import wrapsdeftransactional(func): @wraps(func)defwrapper(*args, **kwargs):# args[0] is obj state = memento(args[0])try: func(*args, **kwargs)except Exception as e: state()raise ereturn wrapper
這是常規裝飾器的寫法,當然我們也可以用類來實現裝飾器,其實原理差不多,只是有一些細節不太一樣。
classTransactional:def__init__(self, method): self.method = methoddef__get__(self, obj, cls):deftransaction(*args, **kwargs): state = memento(obj)try:return self.method(*args, **kwargs)except Exception as e: state()raise ereturn transaction
當我們將這個註解加在某一個類方法上,當我們執行 obj.xxx 的時候,就會執行 Transactional 這個類中的__get__方法,而不是獲得 Transactional 這個類。並且把 obj 以及 obj 對應的類型作為參數傳入,也就是這裡的 obj 和 cls 的含義。這個是用類來實現裝飾器的常規做法,我們貼一下常規的代碼,來比較學習一下。
classWrapper:def__init__(self, func): wraps(func)(self)def__call__(self, *args, **kwargs):returnself.__wrapped__(*args, **kwargs)def__get__(self, instance, cls):if instance is None:returnselfelse:return types.MethodType(self, instance)
這是一個用類來實現裝飾器的 case,我們可以看到在__get__這個函數當中返回的是 self,也就是返回了 Wrapper 這個類。類通常是不能直接執行的,為了讓它能夠執行,這裡給它實現了一個__call__函數。用類實現裝飾器也不常見,我們熟悉高階函數的方法就可以了。
實戰
最後我們來看一個實際應用的例子,我們實現了一個 NumObj 的類,兼容了上面兩種事務的使用,可以對比一下看看區別。
classNumObj:def__init__(self, value): self.value = valuedef__repr__(self):return'<%s, %r>' % (self.__class__.__name__, self.value)defincrement(self): self.value += 1 @transactionaldefdo_stuff(self): self.value += '111' self.increment()if __name__ == '__main__': num_obj = NumObj(-1) a_transaction = Transaction(True, num_obj)# 使用Transactiontry: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)# 使用Transactional print('-- now doing stuff') num_obj.increment()try: num_obj.do_stuff()except Exception: print('-> doing stuff failed')import sysimport traceback traceback.print_exc(file=sys.stdout) print(num_obj)
從代碼當中,我們不難發現對於 Transaction 也就是面向對象實現的,我們需要額外創建一個 Transaction 的實例來在 try catch 當中控制是否執行回滾。而使用註解的方式更加靈活,它執行失敗會自動執行回滾,不需要太多的額外操作。
一般來說我們更加喜歡使用註解的方式,因為這樣的方式更加簡潔乾淨,更加pythonic,能夠體現出 Python 的強大。第一種方法顯得有些中規中矩,不過好處是可讀性強一些,代碼實現難度也低一些。
大家如果在實際工作當中有需要用到,可以根據自己的實際情況去進行選擇,兩種都是不錯的方法。
今天的文章就到這裡,衷心祝願大家每天都有所收穫。