初學python--認識裝飾器
目錄:
0x00寫在最前面
0x01裝飾器的作用,應用場景
n0x02函數的定義,使用,嵌套,傳參
n0x03閉包的概念與理解
n0x04裝飾器引入:通過函數嵌套和顯示調用說明,裝飾器調用過程
n0x05裝飾器簡單實例,語法糖@,說明裝飾器怎麼用
n0x06多個裝飾器調用順序
n0x07帶參裝飾器
0x08裝飾器作用於類中函數
0x09類裝飾器
n0x0a幾個實用的裝飾器
0x0b參考文章、資料
正文:
0x00寫在最前面
nn
- 剛開始學裝飾器,哪裡寫的不對,敬請指出。只解釋使用情況,具體原理未進行細究。
- 這篇文章涉及了類和對象的內容,下篇文章會專門說說python中的類和對象。希望大家一起進步。
- 看這篇文章時,希望你能坐在電腦前,打開你的PyCharm,跑一下文章中的代碼,並有能力對其進行修改,效果會好很多。
- 文章同步發布在我的個人博客:ZKeeers Blog
- 歡迎署名和不署名轉載,你就說是你寫的
0x01裝飾器的作用,應用場景
nn
我理解的裝飾器:就像人生來裸露,為了遮羞,幾件單薄衣服就夠了;為了禦寒,可能要多穿幾件,還得加厚;為了美觀,會添點兒修飾,做做造型。我們使用函數時,想在核心功能上根據需求不同添加不同的新功能,這當然不能更改核心功能的代碼,裝飾器的作用就凸顯出來了。裝飾器可以用於插入日誌、數據預處理、許可權校驗、錯誤追蹤、性能測試等場景,提高了代碼重用度
nnnn
0x02 函數的定義,使用,嵌套,傳參
nn
Python中萬物皆對象,函數也是也是對象,什麼是對象呢?可以簡單理解為一個模型,想用就調用,不想用就扔那兒。假設我定義一個函數
nndef on_call():n print(「hello」)n
nn
當我使用
nnnew_func = on_calln
nn
時,我使用了這個對象。
nn當我使用
nnnew_func = on_callnnew_func()n
nn
時,我初始化了一個new_func的變數,並把它指向了on_call這個函數對象,我也可以看作這是實例化的過程。
nn當我使用new_func = on_call()時,我初始化了一個new_func變數並執行on_call函數,將返回值賦值給new_func。
nn對上面的主要理解是:帶括弧和不帶括弧的區別。帶括弧可以理解為我在執行這個函數;不帶括弧可以理解為我在使用這個函數對象。
nn當我定義一個函數時
nndef print_time():n print(「before inner」)n def pinrt_time_inner()n print(「inner」)n print(「after inner」)n
nn
同樣,我也可以將函數作為一個參數傳遞給另一個函數使用。
nndef print_time(func):n print(func.__name__)n
nnnn
0x03閉包的概念與理解
nn
在計算機科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變數的函數。這個被引用的自由變數將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。閉包在運行時可以有多個實例,不同的引用環境和相同的函數組合可以產生不同的實例。[以上引自維基百科] 再通俗來說,閉包就是一個函數的所有語句與執行這個函數的環境的打包。
nn
是不是還不明白,我來舉個栗子說明。
nnnn# func1.pynfunc_name = "func1"nndef func1_call(f):n return f()n
#func2.pynimport decorator_func1nnfunc_name = "func2"nndef func2_print_filename():n print(func_name)nnif __name__ == __main__:n decorator_func1.func1_call(func2_print_filename)n
執行結果為:func2
nnnn在這兒我們可以看出,func2_print_name函數所列印的是它所處環境的變數,儘管是func1調用的func2_print_name。這樣應該看清楚了吧。
nnnn同時,使用函數嵌套時,這個閉包作用顯示的更加明顯,通過下面的例子進行說明。
nnnn# decorator_ex1.pynimport decorator_func1nndef outer():n func_name = "outer"n def print_filename(): n #func_name = "inner"n print(func_name)n decorator_func1.func1_call(print_filename)nnif __name__ == __main__:n outer()n
nn
此時輸出結果為:outer;若取消func_namen= inner的注釋,輸出結果為innner。當執行print(func_name)的時候,python解釋器會按照LEGB順序查找func_name變數:
nnL-----local 局部名字空間
E-----enclosing 直接外圍空間
G-----global 全局名字空間
B-----builtin 內建名字空間
即LEGB規則。
最後總結,閉包的價值在於可以打包函數執行的上下文環境。
0x04裝飾器引入: 通過函數嵌套和顯示調用說明,裝飾器調用過程
nn
根據上面說到的,我如下定義一個核心功能函數,用於輸出時間。我想使用裝飾器添加日誌。那我應該這麼做。
nnimport timenndef decorator_log_ex(func): n print("before: %s" % (func.__name__))n func() n print("after: %s" % (func.__name__))nndef Time_func_ex():n print("time: %s"%time.ctime())nnif __name__ == __main__:n decorator_log_ex(Time_func_ex)n
nn
這樣執行結果為:
nnbefore: Time_func_exntime: Tue Jun 20 09:24:25 2017nafter: Time_func_exn
nn
這樣,只要將核心功能函數傳遞進去就可以實現插入日誌了。這就是裝飾器的基礎實現。
nn0x05裝飾器簡單實例,語法糖@,說明裝飾器怎麼用
nn
按照上面的調用,每次都這麼寫,是不是太麻煩了。Python提供了一種更加簡潔優雅的寫法,人們稱之為語法糖,如下代碼所示:
nnimport timenndef decorator_log_ex(func):n print("before: %s" % (func.__name__))n func()n print("after: %s" % (func.__name__))nn@ decorator_log_exndef Time_func_ex():n print("time: %s"%time.ctime())nnif __name__ == __main__:n Time_func_ex()n
nn
這樣,寫法什麼的就簡潔多了。但執行的時候,仍然是轉換為
decorator_log_ex(Time_func_ex)n
執行。
上面用來插入日誌的裝飾器,寫成類的形式,可以這麼寫。
import timennclass decorator_log_ex:n def __init__(self, func):n self.func = funcnn def __call__(self, *args):n print("before: %s" % (self.func.__name__))n self.func(*args)n print("after: %s" % (self.func.__name__))nn@decorator_log_exndef Time_func_ex():n print("time: %s" % time.ctime())nnif __name__ == __main__:n Time_func_ex()n
利用類的內建函數__call__(),同時類初始化要添加函數參數。這樣一來,裝飾器的功能強大了不少。
nn
0x06多個裝飾器調用順序
nn
那我同時使用多個裝飾器的時候,裝飾器的調用順序是怎樣的呢?
nn通過以下的例子進行說明:
nnnndef decorated_a(func):n def wapper(*args, **kwargs):n func()n print(", 1", end="")n return wappernndef decorated_b(func):n def wapper(*args, **kwargs):n func()n print(", 2", end="")n return wappernndef decorated_c(func):n def wapper(*args, **kwargs):n func()n print(", 3", end="")n return wappernn# 裝飾器的執行順序n@decorated_cn@decorated_bn@decorated_andef on_call():n print("報數", end="")nnif __name__ == __main__:n on_call()n
nn
最後執行結果:報數, 1, 2, 3。所以說以上on_call函數的實際上時這樣執行的:
on_call = decorator_c(decorator_b(decorator__a(on_call)))n
0x07帶參裝飾器
nn
如果裝飾器本身需要傳入參數,那就需要編寫一個返回decorator的高階函數,寫出來會更複雜。比如,要自定義log的文本:
nn# 帶參裝飾器ndef log(action):n def decorator_log(func):n def set_log(*args, **kwargs):n print("狀態:%s 函數對象:%s" %(action, func.__name__))n return func()n return set_logn return decorator_lognnn#帶參裝飾器使用n@log("Running")ndef print_time():n print(time.ctime())nnif __name__ == __main__:n print_time()n
nnnn
最後輸出結果為:
nn狀態:Running 函數對象:print_timenTue Jun 20 09:57:06 2017n
相比於不帶參數的兩層嵌套的裝飾器,帶參數的裝飾器實際調用是這樣的:
log(「Running」)(print_time)n
首先調用log(「Running」),返回值是decorator_log函數,再調用返回的decorator_log函數,參數是print_time,此次返回值為:set_log函數。然後執行set_log函數。
nnnn0x08裝飾器作用於類中函數
主講使用外部定義的裝飾器,作用於類內部函數,同時在在裝飾器中調用類中成員函數。
使用一個捕獲異常例子進行說明。
下面有一個類:# -*-encoding:utf-8-*-ndef catch_exception(func):n def handle_exception(*args, **kwargs):n try:n u = func(*args, **kwargs)n return un except BaseException as e:n return "Catch an exception."nn return handle_exceptionnnnclass my_deco:n def __init__(self):n passnn def print_info(self):n print("這是個測試類")nn @catch_exceptionn def get_result(self, val):n print("result: ", 10 / val)nnnif __name__ == __main__:n t = my_deco()n t.get_result(1)n
定義一個catch_exception函數,用以處理這個函數接收的參數也就是func執行時所拋出的異常。定義在類外面,可以正常在類裡面使用。
但是當我想要拋出異常時,採用類成員函數進行處理,這時候catch_exception函數需要訪問類的成員函數,只要在catch_exception的參數列表中,加入self參數即可。上面的例子可以修改為:
# -*-encoding:utf-8-*-ndef catch_exception(func):n def handle_exception(self, *args, **kwargs):n try:n u = func(self, *args, **kwargs)n return un except BaseException as e:n self.print_info()n return "Catch an exception."nn return handle_exceptionnnnclass my_deco:n def __init__(self):n passnn def print_info(self):n print("這是個測試類")nn @catch_exceptionn def get_result(self, val):n print("result: ", 10 / val)nnnif __name__ == __main__:n t = my_deco()n t.get_result(0)n
這樣就可以用類成員函數正常處理拋出異常了,而使用時不需要進行任何修改。
0x09類裝飾器
類裝飾器類似於函數裝飾器的概念,但它應用於類,它們可以用於管理類自身,或者用來攔截實例創建調用以管理實例。
00 管理類實例
類裝飾器可以攔截實例創建調用,所以它們可以用來管理一個類的所有實例,或者擴展這些實例的介面。下面的類裝飾器例子實現了傳統的單體編碼模式,即最多只有一個類的一個實例存在。
instances = {} # 全局變數,管理實例nnndef getInstance(aClass, *args):n if aClass not in instances: #如果不存在此實例便記錄;否則直接返回已創建的實例n instances[aClass] = aClass(*args)n return instances[aClass] # 每一個類只能存在一個實例nnndef singleton(aClass):n def onCall(*args):n return getInstance(aClass, *args)nn return onCallnnn@singleton # Person = singleton(Person)nclass Person:n def __init__(self, name, hours, rate):n self.name = namen self.hours = hoursn self.rate = ratenn def pay(self):n return self.hours * self.ratennn@singleton # Spam = singleton(Spam)nclass Spam:n def __init__(self, val):n self.attr = valnnnif __name__ == __main__:n bob = Person(Bob, 40, 10)n print(bob.name, bob.pay())nn sue = Person(Sue, 50, 20)n print(sue.name, sue.pay())nn X = Spam(42)n Y = Spam(99)n print(X.attr, Y.attr)n
當Person或Spam類用來創建一個實例的時候,裝飾器把實例構建調用指向了onCall,它反過來調用getInstance,以針對每個類管理並分享一個單個實例,而不管進行了多少次構建調用。運行結果:
C:Anaconda3python.exe E:/ProgramList/PYCHARM/CorePython/decorator_class.pynBob 400nBob 400n42 42nnProcess finished with exit code 0n
01私有屬性禁止訪問
實現類似於java/c++中私有屬性禁止訪問的功能,不接受裝飾器這個類外部對私有屬性的訪問,但是類中可以自由的對這些私有屬性進行訪問。def Private(*privates):n def onDecorator(aClass):n class onInstance:n def __init__(self, *args, **kargs):n self.wrapped = aClass(*args, **kargs)nn def __getattr__(self, attr):n print(get:, attr)n if attr in privates: #如果在私有變數列表中提示錯誤;否則可以訪問n raise TypeError(private attribute fetch: + attr)n else:n return getattr(self.wrapped, attr)nn def __setattr__(self, attr, value):n print(set:, attr, value)n if attr == wrapped: # 這裡捕捉對wrapped的賦值n self.__dict__[attr] = valuen elif attr in privates:n raise TypeError(private attribute change: + attr)n else: # 這裡捕捉對wrapped.attr的賦值n setattr(self.wrapped, attr, value)nn return onInstancenn return onDecoratornnn@Private(data, size)nclass Doubler:n def __init__(self, label, start):n self.label = labeln self.data = startnn def size(self):n return len(self.data)nn def double(self):n for i in range(self.size()):n self.data[i] = self.data[i] * 2nn def display(self):n print(%s => %s % (self.label, self.data))nnnif __name__ == __main__:n traceMe = Truenn X = Doubler(X is, [1, 2, 3])n Y = Doubler(Y is, [-10, -20, -30])nn print(X.label)n X.display()n X.double()n X.display()nn print(Y.label)n Y.display()n Y.double()n Y.label = Spamn Y.display()nn # 下面這些訪問都會引發異常n print(X.size())n print(X.data)nn X.data = [1, 1, 1]n X.size = lambda S: 0n print(Y.data)n print(Y.size())n
運行結果:
C:Anaconda3python.exe E:/ProgramList/PYCHARM/CorePython/decorator_class_2.pynset: wrapped <__main__.Doubler object at 0x000001E43165C7F0>nset: wrapped <__main__.Doubler object at 0x000001E43165C898>nget: labelnX isnget: displaynX is => [1, 2, 3]nget: doublenget: displaynX is => [2, 4, 6]nget: labelnY isnget: displaynY is => [-10, -20, -30]nget: doublenset: label Spamnget: displaynSpam => [-20, -40, -60]nget: sizenTraceback (most recent call last):n File "E:/ProgramList/PYCHARM/CorePython/decorator_class_2.py", line 64, in <module>n print(X.size())n File "E:/ProgramList/PYCHARM/CorePython/decorator_class_2.py", line 10, in __getattr__n raise TypeError(private attribute fetch: + attr)nTypeError: private attribute fetch:sizennProcess finished with exit code 1n
產生了報錯,錯誤類型及錯誤信息是自己定義的那樣。
0x0a幾個實用的裝飾器
nn
超時函數
這個函數的作用在於可以給任意可能會hang住的函數添加超時功能,這個功能在編寫外部API調用 、網路爬蟲、資料庫查詢的時候特別有用
timeout裝飾器的代碼如下:
import signal,functools #下面會用到的兩個庫 nclass TimeoutError(Exception): pass #定義一個Exception,後面超時拋出 nndef timeout(seconds, error_message = Function call timed out):n def decorated(func):n def _handle_timeout(signum, frame):n raise TimeoutError(error_message)n def wrapper(*args, **kwargs):n signal.signal(signal.SIGALRM, _handle_timeout)n signal.alarm(seconds)n try:n result = func(*args, **kwargs)n finally:n signal.alarm(0)n return resultn return functools.wraps(func)(wrapper)n return decoratedn
使用:
@timeout(5) #限定下面的slowfunc函數如果在5s內不返回就強制拋TimeoutError Exception結束 ndef slowfunc(sleep_time):n import timen time.sleep(sleep_time) #這個函數就是休眠sleep_time秒 nnslowfunc(3) #sleep 3秒,正常返回 沒有異常 nnnslowfunc(10) #被終止 nn## 輸出 n---------------------------------------------------------------------------nTimeoutError Traceback (most recent call last)n
Trace函數
有時候出於演示目的或者調試目的,我們需要程序運行的時候列印出每一步的運行順序 和調用邏輯。類似寫bash的時候的bash -x調試功能,然後Python解釋器並沒有 內置這個時分有用的功能,那麼我們就「自己動手,豐衣足食」。
Trace裝飾器的代碼如下:
import sys,os,linecachendef trace(f):n def globaltrace(frame, why, arg):n if why == "call": return localtracen return Nonen def localtrace(frame, why, arg):n if why == "line":n # record the file name and line number of every trace n filename = frame.f_code.co_filenamen lineno = frame.f_linenon bname = os.path.basename(filename)n print "{}({}): {}".format( bname,n lineno,n linecache.getline(filename, lineno).strip(rn)),n return localtracen def _f(*args, **kwds):n sys.settrace(globaltrace)n result = f(*args, **kwds)n sys.settrace(None)n return resultn return _fn
使用:
@tracendef xxx():n print 1n print 22n print 333nnxxx() #調用 nn## 輸出 n<ipython-input-4-da50741ac84e>(3): print 1 # @trace 的輸出 n1n<ipython-input-4-da50741ac84e>(4): print 22 # @trace 的輸出 n22n<ipython-input-4-da50741ac84e>(5): print 333 # @trace 的輸出 n333n
0x0b參考資料、文章
1.裝飾器-廖雪峰的官方網站
2.如何理解Python裝飾器? - 知乎
3.Python 里為什麼函數可以返回一個函數內部定義的函數?
4.Python--編寫類裝飾器 - Gavin - 博客頻道 - CSDN.NET
5.Python 裝飾器裝飾類中的方法
6.Python--編寫函數裝飾器 - Gavin - 博客頻道 - CSDN.NET
推薦閱讀:
※python的descriptor的意圖是什麼,想知道python當初弄出功能的意圖?困擾我好久了。
※Python 的 Metaclass 有沒有什麼好的 Best Practice 可以學習?
※拆代碼學演算法之用python實現KNN過程詳解
※Python黑帽編程 3.3 MAC洪水攻擊