本篇文章給大家?guī)?lái)了關(guān)于python的相關(guān)知識(shí),其中主要介紹了關(guān)于裝飾器的相關(guān)問(wèn)題,包括了閉包、裝飾器、使用多個(gè)裝飾器、帶參數(shù)的裝飾器等等內(nèi)容,下面一起來(lái)看一下,希望對(duì)大家有幫助。
推薦學(xué)習(xí):python視頻教程
一、閉包
要了解什么是裝飾器(decorator),我們首先需要知道閉包(closure)的概念。
閉包,又稱閉包函數(shù)或者閉合函數(shù),通俗一點(diǎn)來(lái)講,當(dāng)某個(gè)函數(shù)被當(dāng)成對(duì)象返回時(shí)還夾帶了外部變量,就形成了一個(gè)閉包。
以打印Hello World為例,我們先來(lái)看一下嵌套函數(shù)的結(jié)構(gòu)應(yīng)該是什么樣的:
def print_msg(msg): def printer(): print(msg) printer()print_msg('Hello World')# Hello World
執(zhí)行 print_msg('Hello World')
相當(dāng)于執(zhí)行了 printer()
,也就是執(zhí)行 print(msg)
,所以將輸出 Hello World
。
我們?cè)賮?lái)看一下如果是閉包,該是什么樣的結(jié)構(gòu):
def print_msg(msg): def printer(): print(msg) return printer my_msg = print_msg('Hello World')my_msg()# Hello World
本例中的
printer
函數(shù)就是閉包。
執(zhí)行 print_msg('Hello World')
實(shí)際上是返回了如下這樣一個(gè)函數(shù),它夾帶了外部變量 'Hello World'
:
def printer(): print('Hello World')
于是調(diào)用 my_msg
就相當(dāng)于執(zhí)行 printer()
。
那么如何判斷一個(gè)函數(shù)是否是閉包函數(shù)呢?閉包函數(shù)的 __closure__
屬性里面定義了一個(gè)元組用于存放所有的cell對(duì)象,每個(gè)cell對(duì)象保存了這個(gè)閉包中所有的外部變量。而普通函數(shù)的 __closure__
屬性為 None
。
def outer(content): def inner(): print(content) return innerprint(outer.__closure__) # Noneinner = outer('Hello World')print(inner.__closure__) # (<cell at 0x0000023FB1FD0B80: str object at 0x0000023FB1DC84F0>,)
由此可見(jiàn) outer
函數(shù)不是閉包,而 inner
函數(shù)是閉包。
我們還可以查看閉包所攜帶的外部變量:
print(inner.__closure__[0].cell_contents)# Hello World
說(shuō)了那么多,那么閉包究竟有什么用呢?閉包存在的意義就是它夾帶了外部變量(私貨),如果它不夾帶私貨,那么就和普通的函數(shù)沒(méi)有任何區(qū)別。
閉包的優(yōu)點(diǎn)如下:
- 局部變量無(wú)法共享和長(zhǎng)久的保存,而全局變量可能造成變量污染,閉包既可以長(zhǎng)久的保存變量又不會(huì)造成全局污染。
- 閉包使得函數(shù)內(nèi)局部變量的值始終保持在內(nèi)存中,不會(huì)在外部函數(shù)調(diào)用后被自動(dòng)清除。
二、裝飾器
我們先考慮這樣一個(gè)場(chǎng)景,假設(shè)先前編寫的一個(gè)函數(shù)已經(jīng)實(shí)現(xiàn)了4個(gè)功能,為簡(jiǎn)便起見(jiàn),我們用 print
語(yǔ)句來(lái)代表每一個(gè)具體的功能:
def module(): print('功能1') print('功能2') print('功能3') print('功能4')
現(xiàn)在,由于某種原因,你需要為 module
這個(gè)函數(shù)新增一個(gè) 功能5
,你完全可以這樣修改:
def module(): print('功能1') print('功能2') print('功能3') print('功能4') print('功能5')
但在現(xiàn)實(shí)業(yè)務(wù)中,直接做出這樣的修改往往是比較危險(xiǎn)的(會(huì)變得不易于維護(hù))。那么如何在不修改原函數(shù)的基礎(chǔ)上去為它新添一個(gè)功能呢?
你可能已經(jīng)想到了使用之前的閉包知識(shí):
def func_5(original_module): def wrapper(): original_module() print('功能5') return wrapper
func_5
代表該函數(shù)主要用于實(shí)現(xiàn) 功能5
,我們接下來(lái)將 module
傳入進(jìn)去來(lái)觀察效果:
new_module = func_5(module)new_module()# 功能1# 功能2# 功能3# 功能4# 功能5
可以看出,我們的新模塊:new_module
已經(jīng)實(shí)現(xiàn)了 功能5
。
在上面的例子中,函數(shù)
func_5
就是一個(gè)裝飾器,它裝飾了原來(lái)的模塊(為它新添了一個(gè)功能)。
當(dāng)然,Python有更簡(jiǎn)潔的寫法(稱之為語(yǔ)法糖),我們可以將@符號(hào)與裝飾器函數(shù)的名稱一起使用,并將其放置在要裝飾的函數(shù)的定義上方:
def func_5(original_module): def wrapper(): original_module() print('功能5') return wrapper@func_5def module(): print('功能1') print('功能2') print('功能3') print('功能4')module()# 功能1# 功能2# 功能3# 功能4# 功能5
基于此,我們可以在不修改原函數(shù)的基礎(chǔ)上完成計(jì)時(shí)任務(wù)(計(jì)算原函數(shù)的運(yùn)行時(shí)間),如下:
def timer(func): def wrapper(): import time tic = time.time() func() toc = time.time() print('程序用時(shí): {}s'.format(toc - tic)) return wrapper@timerdef make_list(): return [i * i for i in range(10**7)]my_list = make_list()# 程序用時(shí): 0.8369960784912109s
事實(shí)上,my_list
并不是列表,直接打印會(huì)顯示 None
,這是因?yàn)槲覀兊?wrapper
函數(shù)沒(méi)有設(shè)置返回值。如果需要獲得 make_list
的返回值,可以這樣修改 wrapper
函數(shù):
def wrapper(): import time tic = time.time() a = func() toc = time.time() print('程序用時(shí): {}s'.format(toc - tic)) return a
三、使用多個(gè)裝飾器
假如我們要為 module
新添 功能5
和 功能6
(按數(shù)字順序),那該如何做呢?
好在Python允許同時(shí)使用多個(gè)裝飾器:
def func_5(original_module): def wrapper(): original_module() print('功能5') return wrapperdef func_6(original_module): def wrapper(): original_module() print('功能6') return wrapper@func_6@func_5def module(): print('功能1') print('功能2') print('功能3') print('功能4')module()# 功能1# 功能2# 功能3# 功能4# 功能5# 功能6
上述過(guò)程實(shí)際上等價(jià)于:
def module(): print('功能1') print('功能2') print('功能3') print('功能4')new_module = func_6(func_5(module))new_module()
此外,需要注意的是,在使用多個(gè)裝飾器時(shí),最靠近函數(shù)定義的裝飾器會(huì)最先裝飾該函數(shù),如果我們改變裝飾順序,則輸出結(jié)果也將改變:
@func_5@func_6def module(): print('功能1') print('功能2') print('功能3') print('功能4')module()# 功能1# 功能2# 功能3# 功能4# 功能6# 功能5
四、被裝飾的函數(shù)帶有參數(shù)
如果被裝飾的函數(shù)帶有參數(shù),那該如何去構(gòu)造裝飾器呢?
考慮這樣一個(gè)函數(shù):
def pide(a, b): return a / b
當(dāng)b=0 時(shí)會(huì)出現(xiàn) ZeropisionError
。如何在避免修改該函數(shù)的基礎(chǔ)上給出一個(gè)更加人性化的提醒呢?
因?yàn)槲覀兊?pide
函數(shù)接收兩個(gè)參數(shù),所以我們的 wrapper
函數(shù)也應(yīng)當(dāng)接收兩個(gè)參數(shù):
def smart_pide(func): def wrapper(a, b): if b == 0: return '被除數(shù)不能為0!' else: return func(a, b) return wrapper
使用該裝飾器進(jìn)行裝飾:
@smart_pidedef pide(a, b): return a / bprint(pide(3, 0))# 被除數(shù)不能為0!print(pide(3, 1))# 3.0
如果不知道要被裝飾的函數(shù)有多少個(gè)參數(shù),我們可以使用下面更為通用的模板:
def decorator(func): def wrapper(*args, **kwargs): # ... res = func(*args, **kwargs) # ... return res # 也可以不return return wrapper
五、帶參數(shù)的裝飾器
我們之前提到的裝飾器都沒(méi)有帶參數(shù),即語(yǔ)法糖 @decorator
中沒(méi)有參數(shù),那么該如何寫一個(gè)帶參數(shù)的裝飾器呢?
前面實(shí)現(xiàn)的裝飾器都是兩層嵌套函數(shù),而帶參數(shù)的裝飾器是一個(gè)三層嵌套函數(shù)。
考慮這樣一個(gè)場(chǎng)景。假如我們?cè)跒?module
添加新功能時(shí),希望能夠加上實(shí)現(xiàn)該功能的開(kāi)發(fā)人員的花名,則可以這樣構(gòu)造裝飾器(以 功能5
為例):
def func_5_with_name(name=None): def func_5(original_module): def wrapper(): original_module() print('功能5由{}實(shí)現(xiàn)'.format(name)) return wrapper return func_5
效果如下:
@func_5_with_name(name='若水')def module(): print('功能1') print('功能2') print('功能3') print('功能4')module()# 功能1# 功能2# 功能3# 功能4# 功能5由若水實(shí)現(xiàn)
對(duì)于這種三層嵌套函數(shù),我們可以這樣理解:當(dāng)為 func_5_with_name
指定了參數(shù)后,func_5_with_name(name='若水')
實(shí)際上返回了一個(gè) decorator
,于是 @func_5_with_name(name='若水')
就相當(dāng)于 @decorator
。
六、使用類作為裝飾器
將類作為裝飾器,我們需要實(shí)現(xiàn) __init__
方法和 __call__
方法。
以計(jì)時(shí)器為例,具體實(shí)現(xiàn)如下:
class Timer: def __init__(self, func): self.func = func def __call__(self): import time tic = time.time() self.func() toc = time.time() print('用時(shí): {}s'.format(toc - tic))@Timerdef make_list(): return [i**2 for i in range(10**7)]make_list()# 用時(shí): 2.928966999053955s
如果想要自定義生成列表的長(zhǎng)度并獲得列表(即被裝飾的函數(shù)帶有參數(shù)情形),我們就需要在 __call__
方法中傳入相應(yīng)的參數(shù),具體如下:
class Timer: def __init__(self, func): self.func = func def __call__(self, num): import time tic = time.time() res = self.func(num) toc = time.time() print('用時(shí): {}s'.format(toc - tic)) return res@Timerdef make_list(num): return [i**2 for i in range(num)]my_list = make_list(10**7)# 用時(shí): 2.8219943046569824sprint(len(my_list))# 10000000
如果要構(gòu)建帶參數(shù)的類裝飾器,則不能把 func
傳入 __init__
中,而是傳入到 __call__
中,同時(shí) __init__
用來(lái)初始化類裝飾器的參數(shù)。
接下來(lái)我們使用類裝飾器來(lái)復(fù)現(xiàn)第五章節(jié)中的效果:
class Func_5: def __init__(self, name=None): self.name = name def __call__(self, func): def wrapper(): func() print('功能5由{}實(shí)現(xiàn)'.format(self.name)) return wrapper@Func_5('若水')def module(): print('功能1') print('功能2') print('功能3') print('功能4')module()# 功能1# 功能2# 功能3# 功能4# 功能5由若水實(shí)現(xiàn)
七、內(nèi)置裝飾器
Python中有許多內(nèi)置裝飾器,這里僅介紹最常見(jiàn)的三種:@classmethod
、@staticmethod
和 @property
。
7.1 @classmethod
@classmethod
用于裝飾類中的函數(shù),使用它裝飾的函數(shù)不需要進(jìn)行實(shí)例化也可調(diào)用。需要注意的是,被裝飾的函數(shù)不需要 self
參數(shù),但第一個(gè)參數(shù)需要是表示自身類的 cls
參數(shù),它可以來(lái)調(diào)用類的屬性,類的方法,實(shí)例化對(duì)象等。
cls
代表類本身,self
代表實(shí)例本身。
具體請(qǐng)看下例:
class A: num = 100 def func1(self): print('功能1') @classmethod def func2(cls): print('功能2') print(cls.num) cls().func1()A.func2()# 功能2# 100# 功能1
7.2 @staticmethod
@staticmethod
同樣用來(lái)修飾類中的方法,使用它裝飾的函數(shù)的參數(shù)沒(méi)有任何限制(即無(wú)需傳入 self
參數(shù)),并且可以不用實(shí)例化調(diào)用該方法。當(dāng)然,實(shí)例化后調(diào)用該方法也是允許的。
具體如下:
class A: @staticmethod def add(a, b): return a + bprint(A.add(2, 3))# 5print(A().add(2, 3))# 5
7.3 @property
使用 @property
裝飾器,我們可以直接通過(guò)方法名來(lái)訪問(wèn)類方法,不需要在方法名后添加一對(duì) ()
小括號(hào)。
class A: @property def printer(self): print('Hello World')a = A()a.printer# Hello World
除此之外,@property
還可以用來(lái)防止類的屬性被修改??紤]如下場(chǎng)景
class A: def __init__(self): self.name = 'ABC'a = A()print(a.name)# ABCa.name = 1print(a.name)# 1
可以看出類中的屬性 name
可以被隨意修改。如果要防止修改,則可以這樣做
class A: def __init__(self): self.name_ = 'ABC' @property def name(self): return self.name_ a = A()print(a.name)# ABCa.name = 1print(a.name)# AttributeError: can't set attribute
推薦學(xué)習(xí):python視頻教程