【python實用特性】- 裝飾器


裝飾器的核心理念——在python中,一切皆對象,所以,函數也是一個對象

1、裝飾器是什麼?有何好處?


  • 定義
    簡單來說,裝飾器,是在不更改程序主體內容的前提下,對其進行功能擴展的解決方案。常用於權限驗證、數據校驗、日誌打印等需求。
  • 優點
    (1) 不用更改程序主體。這意味着主業務邏輯並不會受到影響,減少了修改成本及擴展風險。
    (2) 可對多個有同樣需求的對象進行裝飾,提高了代碼複用性。

2、理解裝飾器的前提-高階函數與嵌套函數


裝飾器的本質是一個函數(類裝飾器也是通過__call__函數來實現),不過與一般函數不同的是,它是一個高階嵌套函數,所以在真正理解裝飾器之前,得先明白什麼是高階函數嵌套函數

2.1 高階函數

 如最開始所說,函數也是一個對象,這意味着可以將它賦給一個變量,而函數本身也可以接受變量作爲參數,所以所謂的高階函數,便是 將一個函數作爲另一個函數的參數的函數
文字有點繞,看個實例

#func:用於接收傳來的函數對象
def high_func(func):
    print('我是一個高階函數,我叫:%s'%high_func.__name__)
    print('我是傳來的函數對象,我叫:%s'%func.__name__)

def normal_func():
    print('我是一個普通函數')
    
#查看normal_func的類型
print(type(normal_func))
#調用high_func函數,將normal_func函數作爲參數
high_func(normal_func)
	
輸出:

<class 'function'>
我是一個高階函數,我叫:high_func
我是傳來的函數對象,我叫:normal_func

 由結果可知,normal_func是一個函數對象,作爲參數賦給了變量func,此時high_func就成了一個高階函數。

2.2 嵌套函數

這個很好理解,就像穿衣服一樣的,一件套一件。程序中,就是一個函數中包含一個或多個函數,可以不停嵌套,但建議不要超過3個。
實例如下:

	#嵌套函數定義
	def first():
	    print('我是第一層')
	    def second():
	        print('我是第二層')
	        def thirst():
	            print('我是第三層')
	        return thirst
	    return second
	#調用,從外到內,層層調用
	f = first()()()

	輸出:
		
	我是第一層
	我是第二層
	我是第三層

3、函數裝飾器


函數裝飾器可根據參數的需求分爲以下三種。

3.1 無參數

這種裝飾器最爲簡單,實例如下:

	# 無參數裝飾器
	def decorator1(func):
	    def wrapper():
	        print('我在被裝飾函數之前執行')
	        func()
	        print('我在被裝飾函數之後執行')
	    return wrapper
	
	@decorator1   #調用裝飾器 @參數器函數名
	def f():
	    print('我是被裝飾的函數')
	    
	輸出:
	
	我在被裝飾函數之前執行
	我是被裝飾的函數
	我在被裝飾函數之後執行

如上,我們定義了一個簡單的裝飾器,裝飾器decorator1與被裝飾的函數f都無參數。它的功能是在被裝飾的函數f執行前後各打印一句話。
裝飾原理: 上面代碼中,使用了@語法糖來調用裝飾器。其原理是將被裝飾的函數f作爲參數,傳入裝飾器函數decorate1中,之後將返回的wrapper函數重新賦給f,即 f = decorator1(f)

3.2 被裝飾的函數帶參數
	# 被裝飾函數帶有參數的裝飾器
	def decorator2(func):
	    def wrapper(*args, **kwargs):
	        print('我是被裝飾函數帶有參數的裝飾器')
	        return func(*args, **kwargs) #因爲原函數f有返回值,所以此處要return
	
	    return wrapper
	
	
	@decorator2
	def f(name):
	    print('我是被裝飾的函數,我有參數name,值爲:%s' % name)
	    return name
	
	
	print('返回值:',f(name='jxc'))
	print('被裝飾的函數是%s' % f.__name__)

	輸出:
	
	我是被裝飾函數帶有參數的裝飾器
	我是被裝飾的函數,我有參數name,值爲:jxc
	被裝飾的函數是wrapper

如上,被裝飾的函數f帶有參數name,所以裝飾器的內部嵌套了一個函數wrapper來接受參數。
但此時出現了一個問題,被裝飾的函數明明是f,怎麼打印出來變成了wrapper呢?很簡單,注意這一句 return wrapper,將內部的wrapper函數返回,再賦給了f,所以此時的f.__name__變成了wrapper。
解決辦法:使用functools 中的wraps裝飾器 ,改寫如下:

	from functools import wraps
	
	#被裝飾函數帶有參數的裝飾器
	def decorator2(func):
	    @wraps(func) #使用wraps裝飾器保存原函數信息
	    def wrapper(*args,**kwargs):
	        print('我是被裝飾函數帶有參數的裝飾器')
	        return func(*args, **kwargs)
	    return wrapper
	
	@decorator2
	def f(name):
	    print('我是被裝飾的函數,我有參數name,值爲:%s'%name)
	    return name
	
	f(name='jxc')

	print('被裝飾的函數是%s'%f.__name__)

	輸出:

		我是被裝飾函數帶有參數的裝飾器
		我是被裝飾的函數,我有參數name,值爲:jxc
		被裝飾的函數是f
3.3 裝飾器帶有參數

通過上面我們知道,兩層嵌套的裝飾器只能接收被裝飾函數的參數。所以要想接收裝飾器傳來的參數,那就得再加一層
3.2的基礎上加個需求,根據成績分數判斷是否及格

#裝飾器帶有參數的裝飾器
def decorator3(grade):
    def get_func(func):
        @wraps(func)
        def wrapper(*args,**kwargs):
            print('我是裝飾器帶有參數的裝飾器')
            words = '恭喜你,及格了' if grade>=60 else '沒及格,下次努力'
            print('本次考了%d分'%grade,words)
            return func(*args, **kwargs)
        return wrapper
    return get_func

@decorator3(grade=60)
def f(name):
    print('我是被裝飾的函數,我有參數name,值爲:%s'%name)

f(name='jxc')

print('被裝飾的函數是%s'%f.__name__)

輸出:
	我是裝飾器帶有參數的裝飾器
	本次考了60分 恭喜你,及格了
	我是被裝飾的函數,我有參數name,值爲:jxc
	被裝飾的函數是f

可見,此時的裝飾器共有三層,最外層用來接收裝飾器參數,中間層接收被裝飾函數對象,最裏層接收被裝飾函數的參數。

4、類裝飾器

顧名思義,類裝飾器是通過類的形式(本質上還是通過__call__函數,將類變爲一個函數來實現)來進行功能擴展的,相比於函數裝飾器而言,它具有高內聚可繼承等特點
實例如下:

	
class decoratorTest():
    def __init__(self,func):
        self.func = func
    def __call__(self,*args,**kwargs):
        print('我是一個類裝飾器,現在執行被裝飾的函數:%s'%self.func.__name__)
        print('傳入的參數:',kwargs)
        return self.func(*args,**kwargs)

@decoratorTest
def f(name):
    print('我是被裝飾的函數,我有參數name,值爲:%s'%name)

f(name='jxc')

輸出:
	我是一個類裝飾器,現在執行被裝飾的函數:f
	傳入的參數: {'name': 'jxc'}
	我是被裝飾的函數,我有參數name,值爲:jxc

__init__接收被裝飾的函數對象,__call__進行邏輯處理,類裝飾器這裏不作深入研究。

5、多個裝飾器的執行順序


一個函數可以被多個裝飾器同時裝飾,實例如下:

	#裝飾器d1
	def d1(func):
	    def warpper1():
	        print('洗臉')
	        func()
	    return warpper1
	#裝飾器d2
	def d2(func):
	    def warpper2():
	        print('刷牙')
	        func()
	    return warpper2
	
	#裝飾器d3
	def d3(func):
	    def warpper3():
	        print('起牀')
	        func()
	    return warpper3
	
	@d3
	@d2
	@d1
	def f():
	    print('上班去嘍')
	
	f()
	
	輸出:
		起牀
		刷牙
		洗臉
		上班去嘍

如上,使用了d1、d2、d3三個裝飾器同時對函數f進行裝飾。

  • 裝飾順序: d1—>d2—>d3
  • 執行順序:d3—>d2—>d1—>f
    可見,執行順序與裝飾順序是相反的,這是爲什麼呢?根據3.1提到的的裝飾原理可以得到下列式子:f = d3(d2(d1(f)))

裝飾步驟:
(1)將原函數f作爲參數傳給d1此時d1中的func等於f
(2)再將d1作爲參數傳給d2,此時d2中的func等於d1
(3)再將d2作爲參數傳給d3,此時d3中的func等於d2
(4)最後將d3的返回函數wrapper3賦給原函數f

執行步驟:
(1)執行f()之後,先執行d3的warpper3,打印起牀,之後調用d2
(2)d2的warpper2打印刷牙,之後調用d1
(3) d1的wrapper1打印洗臉,最後執行原函數f
(4) f 打印 上班去嘍

小結: 需要先執行的裝飾器放上面!

按照自己的理解寫的,內容較多,有些地方的表述可能比較繞或不準確,歡迎留言討論~

參考文章:
[1] 如何理解Python裝飾器?- 知乎
[2] Python 裝飾器執行順序迷思

博主其他系列文章:

[1] 【python實用特性】-切片

[2] 【python實用特性】- 迭代、可迭代對象、迭代器

[3] 【python實用特性】- 列表生成式

[4] 【python實用特性】- yield生成器

[5] Python如何爬取動態網頁數據

[6] Python+selenium實現自動爬取實例

[7] python爬取豆瓣Top250-改進版

[8]【Scrapy爬取實例】- 爬取鏈家網指定城市二手房源信息

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