閉包、裝飾器

在學(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 交換了 corefixCore,貍貓換太子。這下就可以愉快的直接使用 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ù)(這里為 core1core2)作為參數(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)用 core1core2 函數(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)算符 ...。如果直接將 argskwargs 作為參數(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ì) core1core2 函數(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__ 方法

完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容