閉包、裝飾器

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

完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容