python基礎(十五):裝飾器

一、引言

軟件的設計應該遵循開放封閉原則,即對擴展是開放的,而對修改是封閉的。對擴展開放,意味着有新的需求或變化時,可以對現有代碼進行擴展,以適應新的情況。對修改封閉,意味着對象一旦設計完成,就可以獨立完成其工作,而不要對其進行修改。

軟件包含的所有功能的源代碼以及調用方式,都應該避免修改,否則一旦改錯,則極有可能產生連鎖反應,最終導致程序崩潰,而對於上線後的軟件,新需求或者變化又層出不窮,我們必須爲程序提供擴展的可能性,這就用到了裝飾器。

二、裝飾器介紹

’裝飾’代指爲被裝飾對象添加新的功能,’器’代指器具/工具,裝飾器與被裝飾的對象均可以是任意可調用對象。概括地講,裝飾器的作用就是在不修改被裝飾對象源代碼和調用方式的前提下爲被裝飾對象添加額外的功能。裝飾器經常用於有切面需求的場景,比如:插入日誌、性能測試、事務處理、緩存、權限校驗等應用場景,裝飾器是解決這類問題的絕佳設計,有了裝飾器,就可以抽離出大量與函數功能本身無關的雷同代碼並繼續重用。

提示:可調用對象有函數,方法或者類,此處我們單以本章主題函數爲例,來介紹函數裝飾器,並且被裝飾的對象也是函數。

三、裝飾器實現

函數裝飾器分爲:無參裝飾器和有參裝飾兩種,二者的實現原理一樣,都是’函數嵌套+閉包+函數對象’的組合使用的產物

1、無參裝飾器的實現

接下來我們要實現一個裝飾器,終極目標:它能計算任何函數的運行時間。

# 假如下面這個函數就是我們需要計算運行時間的函數:
import time
def index():
	time.sleep(3)
	print("welcome to internet cafe !")
index()
(1)在不改變函數體源代碼和調用方式的前提下,我們能想到下面的辦法
# version one
start = time.time()
index()
end = time.time()
print(end - start)

缺點:代碼冗餘很高,,而且看起來不簡潔。因此我決定把這個抽成一個函數

(2)封裝成函數,解決代碼冗餘
# version two
def time_aculate():
	start = time.time()
	res = index() #res先不用管爲什麼要return,後面的有參裝飾器會解答。
	end = time.time()
	print(end - start)
	return res
time_aculate()

缺點:函數被寫死,只能用於index函數運行時間的計算,且index函數的調用方式也發生了變化。

(3)把函數名寫活

於是我們換一種爲函數體傳值的方式,即將值包給函數

# version three
def timer(func):
	def time_aculate():
		start = time.time()
		res = func()
		end = time.time()
		print(end - start)
		return res
	return time_aculate

這樣我們便可以在不修改被裝飾函數源代碼和調用方式的前提下爲其加上統計時間的功能,只不過需要事先執行一次timer將被裝飾的函數傳入,返回一個閉包函數time_aculate重新賦值給變量名 /函數名index,如下

index = timer(index)  #得到index = time_aculate,把index指向的原始內存地址改成了指向time_aculate內存地址,有人會說,那原始內存地址不就引用計數爲0了嗎?不會的,time_aculate攜帶對外作用域的引用:func = 傳給timer函數的index(原始內存地址)
index() # 執行的是time_aculate(),在time_aculate的函數體內再執行最原始的index

#現在有個wrapper函數需要檢測其執行時間:
wrapper = timer(wrapper)
wrapper()

2、有參裝飾器的實現

上面那種雖然已經很完美了。但是針對函數中參數個數經常發生變化這種需求,那麼這個計算函數運行時間的裝飾器就已經不能滿足我們的需求了。我們應該把裝飾器儘可能的獨立開來,不受函數的變化的影響。

# version four
def timer(func):
	def time_aculate(*args,**kwargs): 
		start = time.time()
		res = func(*args,**kwargs)
		end = time.time()
		print(end - start)
		return res # 爲什麼還需要這個res呢?因爲我們需要把time_aculate僞裝成index函數,index有什麼返回值,必須它也要返回什麼值,結合最後兩行代碼理解。
	return time_aculate

注意:上面這個timer就是一個完美的裝飾器了,爲任何函數可以添加計算其運行時間這個功能。
精髓透析:

(1)把函數參數寫活了
def time_aculate(*args,**kwargs):
	func(*args,**kwargs)
# 這兩行代碼是最關鍵的,無論被測函數參數個數如何變,time_aculate都可以應對,且受func處,index原函數的參數個數和類型的制約。
(2)把返回值寫活了
res = func(*args,**kwargs)
return res
#沒有這個 return res,如果被裝飾器裝飾的函數對象有返回值,那麼你的裝飾器就影響了原函數本身的功能。

3、裝飾器實現總結

三個寫活是裝飾器的精髓所在:把函數名寫活了、把函數參數寫活了、把返回值寫活了

4、語法糖

def timer(func):
	def time_aculate(*args,**kwargs): 
		start = time.time()
		res = func(*args,**kwargs)
		end = time.time()
		print(end - start)
		return res 
	return time_aculate
index = timer(index) # 這行代碼實現了time_aculate函數僞裝成index函數
index()

如果你要調用裝飾器,那麼就肯定會有index = timer(index)這個僞裝成原始函數的代碼,假如我有很多地方都要用到這個裝飾器,那麼都要使用這句話,這樣未免太過麻煩且很無趣,語法糖就是解決這個問題的。

import time
def timer(func):
	def time_aculate(*args,**kwargs):
		start = time.time()
		res = func(*args,**kwargs)
		end = time.time()
		print(end - start)
		return res
	return time_aculate
@timer # 這個就是語法糖,它把下面的index函數名存儲的內存地址傳給timer這個裝飾器。相當於就是自動完成了index = timer(index)。
def index():
	time.sleep(3)
	print("welcome to internet cafe !")

index()

這樣我們的代碼會更簡潔,更舒服。

5、一個函數疊加多個裝飾器(即添加多個附加功能)

import time
def login(time): #這裏的time是在局部名稱空間可以和 import time全局名稱空間重複,且先搜索局部名稱空間。
    def check_user(*args,**kwargs):
        user = input("請輸入您的用戶名:").strip()
        pwd = input("請輸入您的密碼:").strip()
        if user == '吳晉丞' and pwd == '123':
            res = time(*args,**kwargs)
            return res
        else:
            print('登錄失敗')
    return check_user

def timer(func):
	def time_aculate(*args,**kwargs):
		start = time.time()
		res = func(*args,**kwargs)
		end = time.time()
		print('您的登錄時間爲:{}'.format(end - start))
		return res
	return time_aculate
@login	# login後傳參
@timer # timer先傳參
def index():
	time.sleep(3)
	print("welcome to internet cafe !")

index()

執行結果:
在這裏插入圖片描述

上面就是疊加多個裝飾器的使用方法。

流程詳述:在寫程序時,程序就存儲在內存中。程序運行時,先是定義階段,程序中所有函數的內存地址都已經有了。再是執行階段,index函數執行,發現有裝飾器存在,則先把index內存地址傳參給最下面的timer裝飾器函數,執行timer(index),返回一個time_aculate函數的內存地址,因爲還有login裝飾器,再把time_aculate的內存地址傳給login裝飾器函數,執行login(time_aculate),返回一個check_user的內存地址,然後 index = check_user的內存地址。再執行index()也即是check_user的內存地址(),然後一層層的執行即可。

裝飾順序:

index --> timer(func) --> login(time)

執行順序:

check_user() --> time_aculate() -->index()

6、完美僞裝原函數屬性(瞭解即可)

裝飾器其實就是對被裝飾函數添加功能後的一個偷樑換柱,僞裝原函數。上面我們主要從三個層面進行僞裝,函數名、返回值、函數參數。其實光這些層面的僞裝依舊可以發現僞裝後的index和index原函數不是一樣的但是裝飾器的目的是達到了,功能已經添加了。

import time
def login(time):...
def timer(func):...
#@login	# login後傳參
#@timer # timer先傳參
def index():
	'''這是一個網吧主頁!'''
	time.sleep(3)
	print("welcome to internet cafe !")
print(index.__name__)
print(index.__doc__)

原始index函數的名字和註釋文檔屬性信息:
在這裏插入圖片描述取消語法糖的註釋:
在這裏插入圖片描述發現雖然我們在index的內存地址上偷樑換柱了,但相應的index函數的屬性信息也發生了改變。這樣我們僞裝的還不夠徹底。

import time
def login(time): 
    def check_user(*args,**kwargs):
    	# check_user.__name__ = func.__name__ # 僞裝後的index,追根疏源存儲的還是check_user函數的內存地址,因此我們改check_user函數屬性即可
		# check_user.__doc__ = func.__doc__ # func中其實存儲的是原始index函數的內存地址。
        user = input("請輸入您的用戶名:").strip()
        pwd = input("請輸入您的密碼:").strip()
        if user == '吳晉丞' and pwd == '123':
            res = time(*args,**kwargs)
            return res
        else:
            print('登錄失敗')
    return check_user

def timer(func):
	def time_aculate(*args,**kwargs):
		start = time.time()
		res = func(*args,**kwargs)
		end = time.time()
		print('您的登錄時間爲:{}'.format(end - start))
		return res
	return time_aculate
@login	# login後傳參
@timer # timer先傳參
def index():
	time.sleep(3)
	print("welcome to internet cafe !")

index僞裝後,追根溯源還是check_user這個函數的內存地址,因此我們需要在login裝飾器中修改check_user的函數屬性。但是函數屬性有很多很多,我們不可能一個個的賦值,達到兩個函數屬性一模一樣。因此我們需要引進一個裝飾器wraps,幫助我們去幹這件事。

一個裝飾器:

from functools import wraps
import time

def timer(func):
    @wraps(func) #我們需要把check_user函數屬性裝飾成func的函數屬性,因此在被裝飾函數的上面寫下語法糖,並在括號中指定你需要以那個函數的屬性爲模版進行裝飾。
    def time_aculate(*args,**kwargs):
        start = time.time()
        res = func(*args,**kwargs)
        end = time.time()
        print('您的登錄時間爲:{}'.format(end - start))
        return res
    return time_aculate
@timer
def index():
    '''文檔註釋'''
    time.sleep(3)
    print("welcome to internet cafe !")
print(index.__name__)
print(index.__doc__)

在這裏插入圖片描述成功。

from functools import wraps
import time
def login(time): 
	@wraps(func)
    def check_user(*args,**kwargs):
        user = input("請輸入您的用戶名:").strip()
        pwd = input("請輸入您的密碼:").strip()
        if user == '吳晉丞' and pwd == '123':
            res = time(*args,**kwargs)
            return res
        else:
            print('登錄失敗')
    return check_user

def timer(func):
	def time_aculate(*args,**kwargs):
		start = time.time()
		res = func(*args,**kwargs)
		end = time.time()
		print('您的登錄時間爲:{}'.format(end - start))
		return res
	return time_aculate
@login	# login後傳參
@timer # timer先傳參
def index():
	time.sleep(3)
	print("welcome to internet cafe !")

但是對於疊加裝飾器會有問題,因爲func是局部變量。

遺留問題:疊加裝飾器,怎麼使用wraps裝飾器同步原始函數的屬性

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章