在學(xué)習(xí) Python 的時(shí)候,慶幸自己有 JavaScript 的基礎(chǔ),在學(xué)習(xí)過(guò)程中,發(fā)現(xiàn)許多相似的地方,如導(dǎo)包的方式、參數(shù)解包、lambda 表達(dá)式、閉包、引用賦值、函數(shù)作為參數(shù)等。
裝飾器是 Python 語(yǔ)言中的一個(gè)重點(diǎn),要學(xué)習(xí)裝飾器的使用,應(yīng)該首先從閉包看起,以掌握裝飾器的基本原理。
詞法作用域
閉包是函數(shù)能夠訪問(wèn)并記住其所在的詞法作用域,由于 Python 和 JavaScript 都是基于詞法作用域的,所以二者閉包的概念是共通的。
為了說(shuō)明 Python 是基于詞法作用域,而不是動(dòng)態(tài)作用域,我們可以看下面的例子:
def getA():
return a
def test():
a = 100
print(getA())
test()
運(yùn)行結(jié)果為:
Traceback (most recent call last):
File "C:\Users\Charley\Desktop\py\py.py", line 8, in <module>
test()
File "C:\Users\Charley\Desktop\py\py.py", line 6, in test
print(getA())
File "C:\Users\Charley\Desktop\py\py.py", line 2, in getA
return a
NameError: name 'a' is not defined
報(bào)錯(cuò)了~我們?cè)僭?getA 函數(shù)所在的作用域聲明一個(gè)變量 a:
def getA():
return a
a = 10010
def test():
a = 100
print(getA())
test()
運(yùn)行結(jié)果為:
10010
這里輸出了 10010 ,說(shuō)明 getA 函數(shù)是依賴于詞法作用域的,其作用域在函數(shù)定義伊始就決定的,其作用域不受調(diào)用位置的影響。
理解了詞法作用域,就理解了閉包。
閉包
前面說(shuō)到過(guò):閉包是函數(shù)能夠訪問(wèn)并記住其所在的詞法作用域的特性。那么只要函數(shù)擁有這個(gè)特性,這個(gè)函數(shù)就是一個(gè)閉包,理論上,所有函數(shù)都是閉包,因?yàn)樗麄兌伎梢栽L問(wèn)其所在的詞法作用域。像這樣的函數(shù)也是一個(gè)閉包:
a = 100
def iAmClosure():
print(a)
iAmClosure()
理論上是如此,但在實(shí)際情況下,閉包的定義要復(fù)雜一點(diǎn)點(diǎn),但仍然基于前面的理論:如果一個(gè)函數(shù)(外層函數(shù))中返回另外一個(gè)函數(shù)(內(nèi)層函數(shù)),內(nèi)層函數(shù)能夠訪問(wèn)外層函數(shù)所在的作用域,這就叫一個(gè)閉包。
下面是一個(gè)例子:
def outer():
a = 100
def inner():
print(a)
return inner
outer()()
運(yùn)行結(jié)果如下:
100
如上所示,inner 函數(shù)就是一個(gè)閉包。
閉包中調(diào)用參數(shù)函數(shù)
同 JavaScript,Python 中也可以將函數(shù)作為參數(shù)傳遞,由于 Python 是引用傳值,因此實(shí)際上傳入的參數(shù)并不是原始函數(shù)本身,而是原始函數(shù)的一個(gè)引用?;陂]包的特性,我們也可以在閉包中訪問(wèn)(或者說(shuō)調(diào)用)這個(gè)函數(shù):
def outer(fn):
def inner():
# 閉包能夠訪問(wèn)并記住其所在的詞法作用域
# 因此在閉包中可以調(diào)用 fn 函數(shù)
fn()
return inner
def test():
print("We will not use 'Hello World'")
ret = outer(test)
ret()
運(yùn)行結(jié)果:
We will not use 'Hello World'
裝飾器引入
認(rèn)識(shí)了閉包,就可以來(lái)說(shuō)一說(shuō)裝飾器了。想象有這么一種需求:
你所在的公司有一些核心的底層方法,后來(lái)公司慢慢壯大,增加了其他的部門(mén),這些部門(mén)都有自己的業(yè)務(wù),但它們都會(huì)使用這些核心的底層方法,你所在的部門(mén)也是如此。
有一天,項(xiàng)目經(jīng)理找到你,讓你在核心代碼的基礎(chǔ)上加一些驗(yàn)證之類的玩意,但是不能修改核心代碼(否則會(huì)影響到其他的部門(mén)),你該怎么做呢?
首先你可能想到這種方式:
def core():
pass
def fixCore():
doSometing()...
core()
fixCore()
通過(guò)一個(gè)外層函數(shù)將 core 函數(shù)進(jìn)行包裝,在執(zhí)行了驗(yàn)證功能后再調(diào)用 core 函數(shù)。
這時(shí)項(xiàng)目經(jīng)理又說(shuō)了,你不能改變我們的調(diào)用方式呀,我們還是想以 core 的方式進(jìn)行調(diào)用,于是你又修改了代碼:
def core():
pass
tmp = core;
def fixCore():
tmp()
core = fixCore
core()
通過(guò)臨時(shí)函數(shù) tmp 交換了 core 和 fixCore,貍貓換太子。這下就可以愉快的直接使用 core 了。
這是項(xiàng)目經(jīng)理又說(shuō)了,我們需要對(duì)多個(gè)核心函數(shù)進(jìn)行包裝,總不能全部使用變量交換吧,并且這樣很不優(yōu)雅,再想想其他的辦法?
好吧,要求真多!于是你想啊想,想到了閉包的方式:將需要包裝的函數(shù)作為參數(shù)傳入外層函數(shù),外層函數(shù)返回一個(gè)內(nèi)層函數(shù),該函數(shù)在執(zhí)行一些驗(yàn)證操作后再調(diào)用閉包函數(shù)。這樣做的好處是:
- 可以對(duì)任意函數(shù)進(jìn)行包裝,只要將函數(shù)作為參數(shù)傳入外層函數(shù)
- 可以在執(zhí)行外層函數(shù)時(shí)對(duì)返回值進(jìn)行任意命名
你寫(xiě)的代碼是這個(gè)樣子的:
# 外層函數(shù),接收待包裝的函數(shù)作為參數(shù)
def outer(fn):
def inner():
doSometing()...
fn()
return inner
# 核心函數(shù)1
def core1():
pass
# 核心函數(shù)2
def core2():
pass
# core1 重新指向被包裝后的函數(shù)
core1 = outer(core1)
# core2 重新指向被包裝后的函數(shù)
core2 = outer(core2)
# 調(diào)用核心函數(shù)
core1()
core2()
大功告成!簡(jiǎn)直完美。同時(shí)恭喜你,你已經(jīng)實(shí)現(xiàn)了一個(gè)裝飾器,裝飾器的核心原理就是:閉包 + 函數(shù)實(shí)參。
Python 原生裝飾器支持
在上面你已經(jīng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的裝飾器,也知道了裝飾器的基本原理。其實(shí),在 Python 語(yǔ)言中,有著對(duì)裝飾器的原生支持,但核心原理依舊不變,只是簡(jiǎn)化了一些我們的操作:
# 外層函數(shù),接收待包裝的函數(shù)作為參數(shù)
def outer(fn):
def inner():
print("----驗(yàn)證中----")
fn()
return inner
# 應(yīng)用裝飾器
@outer
# 核心函數(shù)1
def core1():
print("----core1----")
# 應(yīng)用裝飾器
@outer
# 核心函數(shù)2
def core2():
print("----core2----")
core1()
core2()
運(yùn)行結(jié)果如下:
----驗(yàn)證中----
----core1----
----驗(yàn)證中----
----core2----
Python 原生的裝飾器支持,省去了傳參和重命名的步驟,應(yīng)用裝飾器時(shí),會(huì)將裝飾器下方的函數(shù)(這里為 core1 和 core2)作為參數(shù),并生成一個(gè)新的函數(shù)覆蓋原始的函數(shù)。
裝飾器函數(shù)的執(zhí)行時(shí)機(jī)
裝飾器函數(shù)(也就是我們前面所說(shuō)的外層函數(shù))在什么時(shí)候執(zhí)行呢?我們可以進(jìn)行簡(jiǎn)單的驗(yàn)證:
def outer(fn):
print("----正在執(zhí)行裝飾器----")
def inner():
print("----驗(yàn)證中----")
fn()
return inner
@outer
def core1():
print("----core1----")
@outer
def core2():
print("----core2----")
運(yùn)行結(jié)果為:
----正在執(zhí)行裝飾器----
----正在執(zhí)行裝飾器----
這里我們并沒(méi)有直接調(diào)用 core1 和 core2 函數(shù),裝飾器函數(shù)執(zhí)行了。也就是說(shuō),解釋器執(zhí)行過(guò)程中碰到了裝飾器,就會(huì)執(zhí)行裝飾器函數(shù)。
多重裝飾器
我們也可以給函數(shù)應(yīng)用多個(gè)裝飾器:
def outer1(fn):
def inner():
print("----outer1 驗(yàn)證中----")
fn()
return inner
def outer2(fn):
def inner():
print("----outer2 驗(yàn)證中----")
fn()
return inner
@outer2
@outer1
def core1():
print("----core1----")
core1()
運(yùn)行結(jié)果如下:
----outer2 驗(yàn)證中----
----outer1 驗(yàn)證中----
----core1----
從輸出效果中可以看到:裝飾器的執(zhí)行是從下往上的,底層裝飾器執(zhí)行完成后返回函數(shù)再傳給上層的裝飾器,以此類推。
給被裝飾函數(shù)傳參
如果我們需要給被裝飾函數(shù)傳參,就需要在裝飾器函數(shù)返回的 inner 函數(shù)上做文章了,讓其代理接受被裝飾器函數(shù)的參數(shù),再傳遞給被裝飾器函數(shù):
def outer(fn):
def inner(*args,**kwargs):
print("----outer 驗(yàn)證中----")
fn(*args,**kwargs)
return inner
@outer
def core(*args,a,b):
print("----core1----")
print(a,b)
core(a = 1,b = 2)
運(yùn)行結(jié)果為:
----outer 驗(yàn)證中----
----core1----
1 2
這里提一下 參數(shù)解包的問(wèn)題:在 inner 函數(shù)中的 * 和 ** 表示該函數(shù)接受的可變參數(shù)和關(guān)鍵字參數(shù),而調(diào)用參數(shù)函數(shù) fn 時(shí)使用 * 和 ** 表示對(duì)可變參數(shù)和關(guān)鍵字參數(shù)進(jìn)行解包,類似于 JavaScript 中的擴(kuò)展運(yùn)算符 ...。如果直接將 args 和 kwargs 作為參數(shù)傳給被裝飾函數(shù),那么被裝飾函數(shù)接收到的只是一個(gè)元組和字典,所以需要在解包后傳入。
對(duì)有返回值的函數(shù)進(jìn)行包裝
如果被包裝函數(shù)有返回值,如何在包裝獲取返回值呢?先看一下下面的例子:
def outer(fn):
def inner():
print("----outer 驗(yàn)證中----")
fn()
return inner
@outer
def core():
return "Hello World"
print(core())
運(yùn)行結(jié)果為:
----outer 驗(yàn)證中----
None
為什么函數(shù)執(zhí)行的返回值是 None 呢?不應(yīng)該是 Hello World 嗎?這是因?yàn)檠b飾的過(guò)程其實(shí)是引用替換的過(guò)程,在裝飾之前,core 變量指向其自初始的函數(shù)體,在裝飾后就重新進(jìn)行了指向,指向到了裝飾器函數(shù)所返回的 inner 函數(shù),我們沒(méi)有給 inner 函數(shù)定義返回值,自然在調(diào)用裝飾后的 core 函數(shù)也是沒(méi)有返回值的。為了讓裝飾后的函數(shù)仍有返回值,我們只需讓 inner 函數(shù)返回被裝飾前的函數(shù)的返回值即可:
def outer(fn):
def inner():
print("----outer 驗(yàn)證中----")
return fn()
return inner
@outer
def core():
return "Hello World"
print(core())
運(yùn)行結(jié)果如下:
----outer 驗(yàn)證中----
Hello World
裝飾器的參數(shù)
有時(shí)候我們想要根據(jù)不同的情況對(duì)函數(shù)進(jìn)行裝飾,可以有以下兩種處理方式:
- 定義多個(gè)不同條件下的裝飾器,根據(jù)條件應(yīng)用不同的裝飾器
- 定義一個(gè)裝飾器,在裝飾器內(nèi)部根據(jù)條件的不同進(jìn)行裝飾
第一種方法很簡(jiǎn)單,這里說(shuō)一下第二種方式。
要在裝飾器內(nèi)部對(duì)不同條件進(jìn)行判斷,我們就需要一個(gè)或多個(gè)參數(shù),將參數(shù)傳入:
# main 函數(shù)接受參數(shù),根據(jù)參數(shù)返回不同的裝飾器函數(shù)
def main(flag):
# flag 為 True
if flag:
def outer(fn):
def inner():
print("立下 Flag")
fn()
return inner
return outer
# flag 為 False
else:
def outer(fn):
def inner():
print("Flag 不見(jiàn)了!")
fn()
return inner
return outer
# 給 main 函數(shù)傳入 True 參數(shù)
@main(True)
def core1():
pass
# 給 main 函數(shù)傳入 False 參數(shù)
@main(False)
def core2():
pass
core1()
core2()
運(yùn)行結(jié)果如下:
立下 Flag
Flag 不見(jiàn)了!
上面我們根據(jù)給 main 傳入不同的參數(shù),對(duì) core1 和 core2 函數(shù)應(yīng)用不同的裝飾器。這里的 main 函數(shù)并不是裝飾器函數(shù),其返回值才是裝飾器函數(shù),我們是根據(jù) main 函數(shù)的返回值對(duì)目標(biāo)函數(shù)進(jìn)行裝飾的。
類作為裝飾器
除了函數(shù),類也可以作為裝飾器,在說(shuō)類作為裝飾器之前,首先需要了解 __call__ 方法。
__call__ 方法
我們創(chuàng)建的實(shí)例也是可以調(diào)用的, 調(diào)用實(shí)例對(duì)象將會(huì)執(zhí)行其內(nèi)部的 __call__ 方法,該方法需要我們手動(dòng)實(shí)現(xiàn),如果沒(méi)有該方法,實(shí)例就不能被調(diào)用:
class Test(object):
def __call__(self):
print("我被調(diào)用了呢")
t = Test()
t()
運(yùn)行結(jié)果:
我被調(diào)用了呢
類作為裝飾器
我們已經(jīng)知道對(duì)象的 __call__ 方法在對(duì)象被調(diào)用時(shí)執(zhí)行,其實(shí)類作為裝飾器的結(jié)果就是將被裝飾的函數(shù)指向該對(duì)象,在調(diào)用該對(duì)象時(shí)就會(huì)執(zhí)行對(duì)象的 __call__ 方法,要想讓被裝飾的函數(shù)執(zhí)行 __call__ 方法,首先會(huì)創(chuàng)建一個(gè)對(duì)象,因此會(huì)連帶調(diào)動(dòng) __new__ 和 __init__ 方法,在創(chuàng)建對(duì)象時(shí),test 函數(shù)會(huì)被當(dāng)做參數(shù)傳入對(duì)象的 __init__ 方法。
class Test(object):
# 定義 __new__ 方法
def __new__(self,oldFunc):
print("__new__ 被調(diào)用了")
return object.__new__(self)
# 定義 __init__ 方法
def __init__(self,oldFunc):
print("__init__ 被調(diào)用了")
# 定義 __call__ 方法
def __call__(self):
print("我被調(diào)用了呢")
# 定義被裝飾函數(shù)
@Test
def test():
print("我是test函數(shù)~~")
test()
運(yùn)行結(jié)果:
__new__ 被調(diào)用了
__init__ 被調(diào)用了
我被調(diào)用了呢
保存原始的被裝飾函數(shù)
裝飾后的 test 函數(shù)指向了新建的對(duì)象,那么有沒(méi)有辦法保存被裝飾之前的原始函數(shù)呢?通過(guò)前面我們已經(jīng)知道,在新建對(duì)象的時(shí)候,被裝飾的函數(shù)會(huì)作為參數(shù)傳入 __new__ 和 __init__ 方法,因此我們可以在這兩個(gè)方法中獲取原始函數(shù)的引用:
class Test(object):
# 定義 __new__ 方法
def __new__(self,oldFunc):
print("__new__ 被調(diào)用了")
return object.__new__(self)
# 定義 __init__ 方法
def __init__(self,oldFunc):
print("__init__ 被調(diào)用了")
self.__oldFunc = oldFunc
# 定義 __call__ 方法
def __call__(self):
print("我被調(diào)用了呢")
self.__oldFunc()
# 定義被裝飾函數(shù)
@Test
def test():
print("我是test函數(shù)~~")
test()
運(yùn)行結(jié)果如下:
__new__ 被調(diào)用了
__init__ 被調(diào)用了
我被調(diào)用了呢
我是test函數(shù)~~
總結(jié)
本文主要講到了 Python 中閉包和裝飾器的概念,主要有以下內(nèi)容:
- Python 是基于詞法作用域
- 閉包是函數(shù)能記住并訪問(wèn)其所在的詞法作用域
- 利用禮包實(shí)現(xiàn)簡(jiǎn)單的裝飾器
- Python 原生對(duì)裝飾器的支持
- 給函數(shù)應(yīng)用多個(gè)裝飾器
- 如何給被裝飾函數(shù)傳參
- 如何給有返回值的函數(shù)應(yīng)用裝飾器
- 如何根據(jù)不同條件為函數(shù)應(yīng)用不同的裝飾器
- 類作為裝飾器的情況以及
__call__方法
完。