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