Python 中的閉包與裝飾器

MirrorTan發表於2019-06-02

函式的裝飾器可以以某種方式增強函式的功能,如在 Flask 中可使用 @app.route('/') 為檢視函式新增路由,是一種十分強大的功能。在表現形式上,函式裝飾器為一種巢狀函式,這其中會涉及到閉包的概念。而在巢狀函式之間,外部函式中的變數相對於內部函式而言為自由變數,使用時可能需要藉助於 nonlocal 關鍵字進行宣告。

nonlocal 宣告

按變數的作用域進行分類,Python 中的變數可分為「全域性變數」、「區域性變數」以及「自由變數」。一般而言,Python 中使用變數前不需要宣告變數,但假定在函式體中賦值的變數為區域性變數 ~ 除非顯示使用 global 將在函式中賦值的變數宣告為全域性變數!

而自由變數則是存在於巢狀函式中的一個概念 ~ 定義在其他函式內部的函式被稱之為巢狀函式 nested function ,巢狀函式可以訪問封閉範圍內(外部函式)的變數。巢狀函式不可以在函式外直接訪問。

在Python中,非本地變數預設僅可讀取,在修改時必須顯式指出其為非本地變數 ~ 自由變數 nonlocal,全域性變數 global

>>> ga = 1
>>> def func():
...     nb = 2
...     def inner():
...         ga += 1
...         nb += 2
...         print('ga is %s, and nb is %s' % (ga, nb))
...     return inner
...
>>> test = func()
Traceback (most recent call last):
...
UnboundLocalError: local variable 'ga' referenced before assignment

未加入全域性變數和自由變數宣告時且使用賦值操作時,inner 函式的變數 ga, nb 預設為區域性變數,會報錯;如註釋掉 ga += 1 後同樣會報錯:

Traceback (most recent call last):
...
UnboundLocalError: local variable 'nb' referenced before assignment

可行改寫如下:

>>> ga = 1
>>> def func():
...     nb = 2
...     def inner():
...         global ga
...         nonlocal nb
...         ga += 1
...         nb += 2
...         print('ga is %s, and nb is %s' % (ga, nb))
...     return inner
...
>>> test = func()
>>> test()
ga is 2, and nb is 4
>>> test()
ga is 3, and nb is 6

通過顯示宣告 ga, nb 分別為「全域性變數」和「自由變數」,此時如預期執行!

閉包

函式內的函式以及其自由變數形成閉包。也即閉包是一種保留定義函式時存在的自由變數的繫結的函式 ~ 這樣在呼叫函式時,繫結的自由變數依舊可用。

閉包可以避免全域性變數的使用以及提供某種形式的資料隱藏。當函式中的變數和函式較少且其中某個功能常用時,使用閉包來進行封裝。當變數和函式更加複雜時,則使用類來實現。

# 計算移動平均值的函式
def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager

那麼此時,make_averager() 函式的第二行 series = [] 到第七行 return total/len(series) 為閉包,變數 seriesaverager() 函式中的自由變數!

# avg 為一個 averager 函式物件 ~ 含自由變數的繫結
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11
# 建立另一個 averager 函式物件
>>> avg2 = make_averager()
>>> avg2(1)
1.0
>>> avg2(18)
9.5
# 檢視 avg, avg2 自由變數中儲存的值
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
>>> avg2.__closure__[0].cell_contents
[1, 18]

函式物件通過 __closure__ 屬性——返回 cell 物件元祖(函式中有多少巢狀函式則該元祖的長度有多長),生成該物件的函式被稱之為閉包函式。

func.__closure__[0].cell_contents: 訪問儲存在 cell 物件中值。

Python Decorators

裝飾器本身是一個可呼叫的物件 ~ 函式或類,其引數為另一個函式(被裝飾的函式)。裝飾器可能會處理被裝飾的函式(如新增一些功能)然後將之返回,或者將之替換為另一個函式或可呼叫物件。這也被稱之為超程式設計 metaprogramming —— 在編譯時改變函式功能。

>>> def make_pretty(func):
...     def inner():
...         print("I got decorated!", end='\t')
...         func()
...     return inner
...
>>> def ordinary():
...     print("I am ordinary!")

# 用 make_pretty 函式裝飾 ordinary 函式
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated!  I am ordinary!

可以作為裝飾器的函式內部都有巢狀的功能函式(用以實現主要功能),並返回內部的巢狀函式。

@make_pretty
def ordinary():
    print("I am ordinary!")

# 等價於
def ordinary():
    print("I am ordinary!")
ordianry = make_pretty(ordinary)

make_pretty(func) 是一個最簡單的裝飾器,它接受一個函式為其引數;內部定義了一個 inner() 函式 ~ 輸出 "I got decorated! " 後執行被裝飾函式(此時 funcinner 閉包中的自由變數);然後返回內部函式 inner

此時,對於被裝飾的函式 ordinary 而言,此時是 inner 的引用:

>>> ordinary()
I got decorated!  I am ordinary!
>>> ordinary
<function make_pretty.<locals>.inner at 0x10aeaa1e0>

除了最簡單的裝飾器之外,還可以將多個裝飾器疊放使用以及對裝飾器引數化:

疊放裝飾器

def star(func):
    def inner(*args, **kwargs):
        print('*' * 30)
        func(*args, **kwargs)
        print('*' * 30)
    return inner

def dollar(func):
    def inner(*args, **kwargs):
        print('$' * 30)
        func(*args, **kwargs)
        print('$' * 30)
    return inner

@star
@doller
def printer(msg):
    print(msg)

printer("Hello world!")

# 結果如下
'''
******************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
Hello world!
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
******************************
'''

# 等價於
def printer(msg):
    print(msg)
printer = star(dollar(printer))

多個裝飾器一同使用相當於逐層巢狀,最上方的為最外層的函式,呼叫時最先開始,且最晚結束。

引數化裝飾器

Python 中將被裝飾函式作為引數傳遞給裝飾器函式。此外,我們可以建立一個裝飾器工廠函式(返回裝飾器的函式),把引數傳遞給它,再應用於要裝飾的函式上:

'''
Fluent Python 示例 7-23
https://github.com/fluentpython/example-code/blob/master/07-closure-deco/registration_param.py
'''
registry = set()

def register(active=True):
    def decorate(func):
        print('running register(active=%s)->decorate(%s)'
              % (active, func))
        if active:
            registry.add(func)
        else:
            registry.discard(func)
        return func
    return decorate

@register(active=False)
def f1():
    print("running f1()")

@register()
def f2():
    print("running f2()")

def f3():
    print("running f3()")

將上述程式碼儲存至 registration_param.py 模組中,匯入時得到結果如下:

>>> import registration_param
running register(active=False)->decorate(<function f1 at 0x103ae0268>)
running register(active=True)->decorate(<function f2 at 0x103ae00d0>)
>>> registration_param.registry
{<function f2 at 0x103ae00d0>}

注意:函式裝飾器在被匯入模組時立即執行,而被裝飾的函式只有在明確呼叫時執行。

常用的裝飾器

Python 內建了三個裝飾器,分別為:property, classmethod, staticmethod

  • property 裝飾器可用於設定類中的私有變數;
  • classmethod 用於設定類方法;
  • staticmethod 用於設定類中的靜態方法。

此外,常用的裝飾器還有 functools 模組中的 wraps, lru_cache, singledispath:

  • functools.wraps():保留被修飾函式原有的一些屬性,如 __name__, __doc__
  • functools.lru_cache():可把耗時的函式結果儲存起來,避免傳入相同的引數重複計算 ~ 可用於優化遞迴演算法;
  • functools.singledispath():會把修飾的普通函式變為泛函式。

參考資料

  1. Programiz Home, Parewa Labs Pvt. Ltd, Python Decorators, 2019/06/01.
  2. Ramalho, Luciano. Fluent Python: clear, concise, and effective programming. " O'Reilly Media, Inc.", 2015.

相關文章