Python:從閉包到裝飾器

yolo2233發表於2018-09-09

閉包

閉包的概念

在一個外函式中定義了一個內函式,內函式裡運用了外函式的臨時變數,並且外函式的返回值是內函式的引用。這樣就構成了一個閉包。[1]

以下給出一個閉包的例子:

def outer():
    a = 10
    def inner():
        b= 10
        print(b)
        print(a)
    return inner
    
if __name__ == '__main__':
    inner_func = outer()
    inner_func()
  
  >> 10
複製程式碼

在這裡a作為outer的區域性變數,一般情況下會在函式結束的時候釋放為a分配到的記憶體。但是在閉包中,如果外函式在結束的時候發現有自己的臨時變數將來會在內部函式中用到,就把這個臨時變數繫結給了內部函式,然後自己再結束。[1]

在inner中,a是一個自由變數(free variable). 這是一個技術術語,指未在本地作用域繫結的變數。[2]

在python中,__code__屬性中儲存著區域性變數的和自由變數的名稱,對於inner函式,其區域性變數和自由變數為:

inner_func = outer()
inner_func.__code__.co_freevars
>> ('a',)
inner_func.__code__.co_varnames
>> ('b',)
複製程式碼

那麼,既然外部函式會把內部變數要用到的變數(即內部函式的自由變數)繫結給內部函式,那麼a的繫結在哪裡?a的繫結在返回函式的inner的__closure__屬性中,其中的cell_contents儲存著真正的值。[2]

inner_func.__closure__[0].cell_contents
>> 10
複製程式碼

綜上,閉包是一種函式,它會保留定義函式時存在的自由變數的繫結,這樣呼叫函式時,雖然定義作用域不可用了,但是仍能使用那些繫結。[2]

更進一步

當我們嘗試在inner改變a的值的時候

def outer():
    a = 10

    def inner():
        # nonlocal a
        b = 10
        print(b)
        a += 1
        print(a)
    return inner


if __name__ == '__main__':
    inner_func = outer()
    inner_func()
    
>> UnboundLocalError: local variable 'a' referenced before assignment
複製程式碼

之所以會出現這個錯誤的關鍵在於:當a是數字或任何不可變型別時,a += 1等價於a = a+1,這會把a變為區域性變數。對於不可變型別如數字,字串,元組來說,只能讀取,不能更新。在a += 1 中,等價于于a = a + 1,這裡隱式建立了一個區域性變數a,這樣a就不再是自由變數了,也不存在在閉包中了。

解決方法:加入nonlocal宣告。它的作用是把變數標記為自由變數,這樣即使在函式中為變數賦予新值,也會變成自由變數。

def outer():
    a = 10

    def inner():
        nonlocal a
        b = 10
        print(b)
        a += 1
        print(a)
    return inner

if __name__ == '__main__':
    inner_func = outer()
    inner_func()
>> 10 
   11
複製程式碼

BINGO!

裝飾器

在裝飾器這一部分主要講解以下幾種情形:

  • 函式裝飾器

  • 類裝飾器

  • 裝飾器鏈

  • 帶引數的裝飾器

裝飾器的作用

裝飾器本質上是一個 Python 函式或類,它可以讓其他函式或類在不需要做任何程式碼修改的前提下增加額外功能,裝飾器的返回值也是一個函式/類物件。它經常用於有切面需求的場景,比如:插入日誌、效能測試、事務處理、快取、許可權校驗等場景,裝飾器是解決這類問題的絕佳設計。有了裝飾器,我們就可以抽離出大量與函式功能本身無關的雷同程式碼到裝飾器中並繼續重用。[3]

在網上,各種用的比較多的案例的是,如果我們有非常多的函式,我們現在希望統計每一個函式的執行時間以及列印其引數應該怎麼做。

比較智障的方法是:修改函式原來的內容,加上統計時間的程式碼和列印引數程式碼。但結合前面的閉包的知識,我們應該可以提出這樣一種方法

def count_time(func):
    def wrapper(*args, **kwargs):
        tic = time.clock()
        func(*args, **kwargs)
        toc = time.clock()
        print('函式 %s 的執行時間為 %.4f' %(func.__name__, toc-tic))
        print('引數為:'+str(args)+str(kwargs))
    return wrapper


def test_func(*args, **kwargs):
    time.sleep(1)


if __name__ == '__main__':
    f = count_time(test_func)
    f(['hello', 'world'], hello=1, world=2)
複製程式碼

在這裡func會繫結給wrapper函式,所以即使count_time函式結束了,其中傳入的func也會繫結給wrapper.下述程式碼可驗證之。

f.__code__.co_freevars
>> ('func',)
f.__closure__[0].cell_contents
>> <function test_func at 0x0000014234165AE8>
複製程式碼

而上述的程式碼就是python裝飾器的原理,只不過在我們可以使用 @語法糖簡化程式碼。

def count_time(func):
    def wrapper(*args, **kwargs):
        tic = time.clock()
        func(*args, **kwargs)
        toc = time.clock()
        print('函式 %s 的執行時間為 %.4f' %(func.__name__, toc-tic))
        print('引數為:'+str(args)+str(kwargs))
    return wrapper

@count_time
def test_func(*args, **kwargs):
    time.sleep(1)


if __name__ == '__main__':
    test_func(['hello', 'world'], hello=1, world=2)
    
>> 函式 test_func 的執行時間為 1.9999
    引數為:(['hello', 'world'],){'hello': 1, 'world': 2}

複製程式碼

一個函式同樣也可以被多個裝飾器裝飾,其原理於單個裝飾器相同。重要的是理解裝飾器實際上的呼叫順序。

def count_time(func):
    print('count_time_func')

    def wrapper_in_count_time(*args, **kwargs):
        tic = time.clock()
        func(*args, **kwargs)
        toc = time.clock()
        running_time = toc - tic
        print('函式 %s 執行時間 %f'% (func.__name__, running_time))
    return wrapper_in_count_time


def show_args(func):
    print('show_args func')

    def wrapper_in_show_args(*args, **kwargs):
        print('函式引數為'+str(args)+str(kwargs))
        return func()
    return wrapper_in_show_args


@count_time
@show_args
def test_func(*args, **kwargs):
    print('test_func')


if __name__ == '__main__':
    f = test_func(['hello', 'world'], hello=1, world=2)
    
>> show_args func
    count_time_func
    函式引數為(['hello', 'world'],){'hello': 1, 'world': 2}
    test_func
    函式 wrapper_in_show_args 執行時間 0.000025
複製程式碼

先忽視@count_time裝飾器,假如只有@show_args裝飾器。 那麼,裝飾器背後其實是這樣的:

f = show_args(test func)
f(...)

# 加上@count_time後
f = show_args(test func)
g = count_time(f)
g(...)
複製程式碼

我們可以列印下f,g看下返回的是什麼。

f.__name__
>> wrapper_in_show_args
g.__name__
>> wrapper_in_count_time
複製程式碼

所以整個函式的執行流程是: 首先呼叫了show_args,show_args列印了'show_args_func',之後返回wrapper_in_show_args。接著呼叫count_time,並把wrapper_in_show_args傳給了count_time,首先列印'count_time_func', 之後返回wrapper_in_count_time.最後使用者呼叫wrapper_in_count_time函式,並傳入了相關引數。在wrapper_in_count_time函式裡,首先呼叫了func函式,這裡的func是一個自由變數,即之前傳入的wrapper_in_show_args,所以列印函式引數。在wrapper_in_show_args裡,呼叫了func(),這裡的func又是之前傳入的test_func,所以列印'test_func'。最後列印函式執行時間,整個呼叫過程結束。

總而言之,裝飾器的核心就是閉包,只要理解了閉包,就能理解透徹裝飾器。

另外裝飾器不僅可以是函式,還可以是類,相比函式裝飾器,類裝飾器具有靈活度大、高內聚、封裝性等優點。使用類裝飾器主要依靠類的__call__方法,當使用 @ 形式將裝飾器附加到函式上時,就會呼叫此方法。[3]

class deco_class(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('初始化裝飾器')
        self.func(*args, **kwargs)
        print('中止裝飾器')


@deco_class
def klass(*args, **kwargs):
    print(args, kwargs)


if __name__ == '__main__':
    klass(['hello', 'world'], hello=1, world=2)
>> 初始化裝飾器
    (['hello', 'world'],) {'hello': 1, 'world': 2}
    中止裝飾器
複製程式碼

參考資料

[1] www.cnblogs.com/Lin-Yi/p/73…

[2] Fluent Python, Luciano Ramalho

[3] foofish.net/python-deco…

[4] blog.apcelent.com/python-deco…

相關文章