1. 程式人生 > 實用技巧 >設計模式,你相信嗎,只用兩個函式實現事務!

設計模式,你相信嗎,只用兩個函式實現事務!

大家好,今天給大家介紹一個新的設計模式,叫做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函式,我們先來看程式碼吧。

fromcopyimportcopy,deepcopy

defmemento(obj,deep=False):
state=deepcopy(obj.__dict__)ifdeepelsecopy(obj.__dict__)

defrestore():
obj.__dict__.clear()
obj.__dict__.update(state)

returnrestore

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=deep
self.targets=targets
self.commit()

defcommit(self):
self.states=[memento(target,self.deep)fortargetinself.targets]

defrollback(self):
fora_stateinself.states:
a_state()

由於我們需要事務的物件可能不止一個,所以這裡的targets設計成了陣列的形式。

Transaction裝飾器

我們也可以把事務實現成裝飾器,這樣我們可以通過註解的方式來使用。

這裡的程式碼原理也是一樣的,只不過實現邏輯基於裝飾器而已。如果對裝飾器熟悉的同學,其實也不難理解。這裡的args[0]其實就是某一個類的例項,也就是我們需要保證事務的主體。

fromfunctoolsimportwraps

deftransactional(func):
@wraps(func)
defwrapper(*args,**kwargs):
#args[0]isobj
state=memento(args[0])
try:
func(*args,**kwargs)
exceptExceptionase:
state()
raisee
returnwrapper

這是常規裝飾器的寫法,當然我們也可以用類來實現裝飾器,其實原理差不多,只是有一些細節不太一樣。

classTransactional:

def__init__(self,method):
self.method=method

def__get__(self,obj,cls):
deftransaction(*args,**kwargs):
state=memento(obj)
try:
returnself.method(*args,**kwargs)
exceptExceptionase:
state()
raisee
returntransaction

當我們將這個註解加在某一個類方法上,當我們執行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):
ifinstanceisNone:
returnself
else:
returntypes.MethodType(self,instance)

這是一個用類來實現裝飾器的case,我們可以看到在__get__這個函式當中返回的是self,也就是返回了Wrapper這個類。類通常是不能直接執行的,為了讓它能夠執行,這裡給它實現了一個__call__函式。如果還是看不明白也沒有關係,可以忽略這部分。用類實現裝飾器也不常見,我們熟悉高階函式的方法就可以了。

實戰

最後我們來看一個實際應用的例子,我們實現了一個NumObj的類,相容了上面兩種事務的使用,可以對比一下看看區別。

classNumObj:
def__init__(self,value):
self.value=value

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

defincrement(self):
self.value+=1

@transactional
defdo_stuff(self):
self.value+='111'
self.increment()


if__name__=='__main__':
num_obj=NumObj(-1)

a_transaction=Transaction(True,num_obj)
#使用Transaction
try:
foriinrange(3):
num_obj.increment()
print(num_obj)

a_transaction.commit()
print('----committed')
foriinrange(3):
num_obj.increment()
print(num_obj)
num_obj.value+='x'
print(num_obj)
exceptException:
a_transaction.rollback()
print('----rollback')

print(num_obj)
#使用Transactional
print('--nowdoingstuff')
num_obj.increment()

try:
num_obj.do_stuff()
exceptException:
print('->doingstufffailed')
importsys
importtraceback
traceback.print_exc(file=sys.stdout)

print(num_obj)

從程式碼當中,我們不難發現對於Transaction也就是面向物件實現的,我們需要額外建立一個Transaction的例項來在try catch當中控制是否執行回滾。而使用註解的方式更加靈活,它執行失敗會自動執行回滾,不需要太多的額外操作。

一般來說我們更加喜歡使用註解的方式,因為這樣的方式更加簡潔乾淨,更加pythonic,能夠體現出Python的強大。而第一種方法顯得有些中規中矩,不過好處是可讀性強一些,程式碼實現難度也低一些。大家如果在實際工作當中有需要用到,可以根據自己的實際情況去進行選擇,兩種都是不錯的方法。

今天的文章就到這裡,衷心祝願大家每天都有所收穫。如果還喜歡今天的內容的話,請來一個三連支援吧~(點贊、關注、轉發