寫在前面
本系列目的:希望可以通過一篇文章,不望鞭辟入裏,但求在工程應用中得心應手。
- 裝飾器模式是鼎鼎大名的23種設計模式之一。裝飾器模式可以在不改變原有代碼結構的情況下,擴展代碼功能。
- Python將裝飾器作爲Python的一種特性,內置了對裝飾器的支持,使得Python使用者在使用裝飾器時更加方便,合理使用裝飾器,可以使Python代碼極具美感。
- 由於設計模式是一套被反覆使用的代碼設計經驗,並不是編碼必備的技能。所以在編碼過程中,完全放棄使用裝飾器。但是如果你不寫出pythonic風格的,沒有壞味道的代碼,那麼裝飾器是這條路上繞不過的坎兒。
乾貨兒
包含八節內容:閉包(實現裝飾器的基礎),不帶參數的函數裝飾器,帶參數的函數裝飾器,不帶參數的類裝飾器,帶參數的類裝飾器,常用內建裝飾器,裝飾器總結(套路總結),裝飾器經典實例(單例模式)。
-
閉包
裝飾器是通過閉包實現的。閉包是一個比較複雜的話題,深了說可以講到python對常量表和符號表的處理方式。這裏只做簡單介紹。個人認爲只要記住以下三個特性,就明白了閉包的概念。
- 一個閉包是一個作用域。一個閉包只能訪問作用域內的local變量和作用域外的nonlocal變量。
- 如果在作用域外有和作用域內同名的變量var,如果在作用域內先使用變量var,然後再定義變量var,那麼會拋出a變量先使用後定義的錯誤。
- 閉包可以將作用域"封裝"。那麼我們可以在閉包之外,訪問閉包內的局部變量。因爲局部變量被"封裝"在了閉包內。
-
不帶參數函數裝飾器
假設有一個需求,我們需要在每個函數運行時,打印當下時刻的時間戳。那麼有以下兩種寫法:-
不使用裝飾器
編寫一個打印時間戳的工具函數,編寫一個業務函數。傳入業務函數對象到工具函數中,實現打印時間戳並執行業務函數的需求。代碼如下:import time def f(): print("f is running!") def f1(): print("f1 is running!") def print_running_time(f): print("running time:", time.time()) f() print_running_time(f) print_running_time(f1) >>> ('running time:', 1588864281.154459) f is running! ('running time:', 1588864281.154483) f1 is running!
-
使用裝飾器(函數裝飾器)
編寫一個打印時間戳的裝飾器函數,編寫一個業務函數。裝飾器函數裝飾業務函數,實現打印時間戳並執行業務函數的需求。代碼如下:
import time def print_running_time(f): def wrapper(): print("running time:", time.time()) f() return wrapper @print_running_time def f(): print("f is running!") @print_running_time def f1(): print("f1 is running!") f() f1() >>> ('running time:', 1588864281.154459) f is running! ('running time:', 1588864281.154483) f1 is running!
-
以上兩種寫法對比
通過對比以上兩種寫法,我們可以發現最明顯的區別是代碼在運行時,第一種寫法執行的print_running_time函數,第二種寫法執行的是f函數。那麼明顯第二種寫法中抽象出的語義更加接近我們的業務需求。在同樣需求增加的情況下,第一種寫法需要寫更多的工具函數,並且在執行業務函數時需要進行多層嵌套,極大地增加了代碼的複雜度。第二種寫法可以增加多個裝飾函數裝飾到業務函數上方,在多需求下依舊保持代碼的可讀性和層次感,功能的獨立性和擴展性。-
初探裝飾器原理
裝飾器的代碼運行分爲兩步,裝飾器初始化(在運行至被裝飾函數定義處)和執行被裝飾函數(在運行至被裝飾函數調用處)
以第二種寫法裝飾器的寫法爲例,裝飾器的原理如下:-
在代碼加載過程中,代碼從上往下執行,那麼在執行到#1代碼時,相當於執行了#2代碼。(#1和#2的代碼是等價的。@docorator_func裝飾f,就相當於執行decorator_func(f))。根據#2代碼中print_running_time可知,執行print_running_time(f)的返回值是wrapper(注意返回的是函數對象wrapper,不是wrapper()).
# 1 @print_running_time def f(): print("f is running!") # 2 print_running_time(f) # 3 def print_running_time(f): #3.1 def wrapper(): #3.2 print("running time:", time.time()) #3.3 f() #3.4 return wrapper # 4 f()
-
那麼源碼中#1處的三行代碼,返回值爲wrapper,即相當於通過增加@裝飾函數,f現在已經指向了wrapper對象。
-
根據之前提到閉包的特性:閉包可以訪問作用之外的非局部變量,可以將作用域"封裝",在閉包之外訪問閉包內的變量。所以wrapper可以訪問#3.1中到自己外層函數的參數f變量(被裝飾器函數對象),並且可以封裝wrapper作用域,保存f變量。
-
執行#4處的業務函數f(),即執行#3.2的wrapper()代碼,即執行#3.3和#3.4代碼。
-
整個過程中需要注意的是,在代碼運行至#1時,f作爲裝飾器參數被#3.2wrapper閉包保留,在#1執行完之後,會存在兩個f對象,#4的f對象指向wrapper,#3.4的f對象依舊是#1處的f對象。
-
執行流程爲f()> wrapper()> 執行#7.1 #7.2代碼==>打印當前時刻時間戳,順利執行了原有的業務函數。
-
-
-
-
帶參數的函數裝飾器
現在有新的需求,根據調試和生產環境的不同,需要往復地開關打印時間戳的功能,那麼這時就需要爲裝飾器函數增加參數,來作爲是否打印時間戳的開關。如以下代碼所示,f()會打印當前函數的執行時間,f1()則不會打印函數的執行時間
import time # 1 def print_running_time(*flag): def outer_wrapper(f): def inner_wrapper(): if flag: print("running time:", time.time()) f() return inner_wrapper return outer_wrapper # 2 @print_running_time(1) def f(): print("f is running!") # 3 @print_running_time() def f1(): print("f1 is running!") f() f1() >>> ('running time:', 1588860065.265516) f is running! f1 is running!
帶參數裝飾器原理
- 之前簡單裝飾器原理==>@decorator_func裝飾業務函數f<=>decorator_func(f),那麼#2處的代碼<=>print_running_time(1)(f)<=>outer_wrapper(f)<=>inner_wrapper。需要注意的是inner_wrapper作爲閉包,包含了外層兩個變量flag和f的原始值。
- 接下來調用f(),執行inner_wrapper(),通過判斷flag真假,選擇是否打印當前時間戳,然後執行業務函數,實現需求。
-
不帶參數類裝飾器
-
準確來說,裝飾器的本質是將一個可調用對象作爲參數傳入另一個可調用對象,然後通過閉包保存變量,在適當的時候執行。我們知道,python有兩個特性
- Python中函數和類都是一等對象(這也是裝飾器能作爲python特性的原因之一)。
- python中若callable(obj)爲真,那麼這個對象就是可調用的。所以類,函數,方法,實現了__call__魔術方法的類實例,都是可調用對象。
-
根據裝飾器的本質和以上Python兩個特性可以得出以下結論:
- 函數和類都可以作爲裝飾器,也可以被裝飾器裝飾。
- 類裝飾器和函數裝飾器思路相同,__init__作爲對象初始化的第一步,可以實現一層閉包的效果
-
將之前簡單函數裝飾器的例子換成類裝飾器,代碼如下(爲了與之前代碼保持一致,所以類名不符合Python命名規範):
import time class print_running_time: def __init__(self, f): # 相當於閉包,通過實例屬性保存變量f實現閉包中的變量封裝 self.f = f def __call__(self): # 類實例可以被調用 print("running time:", time.time()) return self.f() @print_running_time def f(): print("f is running!") @print_running_time def f1(): print("f1 is running!") f() f1() >>> 輸出同簡單函數裝飾器
-
-
帶參數的類裝飾器
-
將之前簡單函數裝飾器的例子換成類裝飾器,代碼如下:
import time class print_running_time: def __init__(self, *flag): # 相當於閉包,通過實例屬性保存變量flag實現閉包中的變量封裝 self.flag = flag def __call__(self, f): # 類實例可以被調用,傳入業務函數f def wrapper(): if self.flag: print("running time:", time.time()) f() return wrapper @print_running_time(1) def f(): print("f is running!") @print_running_time() def f1(): print("f1 is running!") f() f1() >>> 輸出同帶參數的函數裝飾器
-
-
常用內建裝飾器
裝飾器是Python最重要的特性之一,Python實現了很多對裝飾器的支持
-
wraps
wraps可以保留被裝飾函數的__doc__。如下代碼所示,wraps裝飾器的開關會導致打印f.__doc__出現兩種結果- 如果註釋掉#1.1的代碼,打印結果爲#1.3
- 如果加上#1.1的代碼,打印結果爲#2.1
import time from functools import wraps # 1 def print_running_time(f): @wraps(f) # 1.1 def wrapper(): '''the func wrapper''' # 1.3 print("running time:", time.time()) f() return wrapper # 2 @print_running_time def f(): '''the func f''' # 2.1 print("f is running!") print(f.__doc__) >>> the func f
-
property 、setter、 deleter
這三個是孿生兄弟,其中property用的最多,setter和deleter依附property。
- property:將函數調用轉化爲屬性
- setter:設置屬性
- deleter:刪除屬性
- 類似於JavaBean,可以將對屬性的操作寫入函數中,限制屬性操作,保護
屬性安全。代碼如下:
class Student(object): @property def name(self): return self._name @name.setter def name(self, name): if len(name) < 2: raise ValueError("無名大俠?") self._name = name @name.deleter def name(self): del self._name stu = Student() stu.name = "劉" # name.setter print(stu.name) ## property del stu.name # name.deleter print(stu.name) # raise AttributeError
-
-
多裝飾器疊加
多個裝飾器疊加是python中很常見的騷操作,如Flask和Django中都會用到,舉例如下:
import sys # 1 def f1(func): print('f1 start') def wrapper(): # 1.1 print('f1 ' + sys._getframe().f_code.co_name + ' start') func() # 1.2 print('f1 ' + sys._getframe().f_code.co_name + ' end') print('f1 end') return wrapper # 2 def f2(func): print('f2 start') def wrapper(): # 2.1 print('f2 ' + sys._getframe().f_code.co_name + ' start') func() # 2.2 print('f2 ' + sys._getframe().f_code.co_name + ' end') print('f2 end') return wrapper # 3 @f1 @f2 def func(): #3.1 print('the func') #4 func() #4.1 >>> f2 start f2 end f1 start f1 end f1wrapper start f2wrapper start the func f2wrapper end f1wrapper end
- 多裝飾器執行過程分析
執行分爲兩步,裝飾器初始化,被裝飾函數執行。順序如下:
- 裝飾器初始化,根據裝飾器原理,#3處的代碼等價於f1(f2(func))
- 執行f2(func); >>> f2 start f2 end; return #2.1處的wrapper(#2.2處的func爲#3.1處的func)
- 執行f1(f2(func))==> f1(#2.1處的wrapper); >>> f1 start f1 end; return #1.1處的wrapper(#1.2處的func爲#2.1處的wrapper)
- 裝飾器初始化結束, 以上兩步的輸出如下:
>>> f2 start f2 end f1 start f1 end
- 被裝飾函數執行:func() <=> #1.1處的wrapper,替換之後代碼如下
#1.2處的func <=> # 2.1處的wrapper,替換之後代碼如下print('f1 ' + sys._getframe().f_code.co_name + ' start') func() # 1.2 print('f1 ' + sys._getframe().f_code.co_name + ' end')
#2.2處的func即爲#4.1處的func,執行以上代碼,輸出結果如下:print('f1 ' + sys._getframe().f_code.co_name + ' start') print('f2 ' + sys._getframe().f_code.co_name + ' start') func() # 2.2 print('f2 ' + sys._getframe().f_code.co_name + ' end') print('f1 ' + sys._getframe().f_code.co_name + ' end')
>>> f1wrapper start f2wrapper start the func f2wrapper end f1wrapper end
- 多裝飾器執行過程分析
-
裝飾器總結
- 裝飾器原理:#1代碼與#2代碼等價
# 1 @decorator_func def func(): pass # 2 decorator_func(func)
- 裝飾器套路
不帶參數的函數裝飾器需要有兩層函數:-
外層函數參數爲被裝飾函數對象
-
內層參數爲被裝飾函數的參數
帶參數的函數裝飾器需要有三層函數:
- 外層函數參數爲裝飾器函數參數(簡直是廢話,外層函數本來就是裝飾器函數)
- 中層函數參數爲被裝飾函數對象
- 內層參數爲被裝飾函數的參數
類裝飾器同理,最外層函數可以用__init_函數代替,中層(如果有和內層函數寫在__call__中
-
- 多個裝飾器疊加
根據業務函數和裝飾器函數的距離,由近及遠執行裝飾器函數(外層函數),然後由遠到近執行內層函數。
- 裝飾器原理:#1代碼與#2代碼等價
-
裝飾器經典實例:單例模式
以下均單進程可行,多線程需要加鎖
單例模式
# eg:1 class Singleton: _singleton = None def __new__(cls): if cls._singleton is None: cls._singleton = super().__new__(cls) return cls._singleton ins1 = Singleton() ins2 = Singleton() print(ins1 is ins2) # eg:2 def singleton(cls): ins_pool = {} def inner(): if cls not in ins_pool: ins_pool[cls] = cls() return ins_pool[cls] return inner @singleton class Cls: def __init__(self): pass ins1 = Cls() ins2 = Cls() print(ins1 is ins2) # eg:3 class Singleton: def __init__(self, cls): self.ins_pool = {} self.cls = cls def __call__(self): print(self.ins_pool) if self.cls not in self.ins_pool: self.ins_pool[self.cls] = self.cls() return self.ins_pool[self.cls] @Singleton class Cls: def __init__(self): pass ins1 = Cls() ins2 = Cls() print(ins1 is ins2)
寫在最後
希望大家可以通過本文掌握裝飾器這個殺手級特性。歡迎關注個人博客:藥少敏的博客