Python 閉包

Python v3.7.0

在函數嵌套的程序結構中,如果內層函數包含對外層函數局部變量的引用,同時外層函數的返回結果又是對內層函數的引用,這就構成了一個閉包。當外層函數在調用結束時,發現自己的局部變量在內層函數中有引用,就會把這個局部變量綁定到內部函數,然後自己再結束。

以一個實現可變參數求和的函數爲例,函數體定義如下:

def calc_sum(*args):
    ax = 0
    for n in args:
        ax += n
    return ax

但是,如果不需要立刻返回求和結構,而是在後面的代碼中,根據需要再計算怎麼辦?此時可以不返回求和的結果,而是返回求和的函數,修改後的代碼如下:

def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax += n
        return ax

    return sum

s = lazy_sum(1, 2, 3, 4, 5)
print(s, type(s),s(),sep='\n')
  
 
# Output>>>
<function lazy_sum.<locals>.sum at 0x10768ed90>
<class 'function'>
15

在上面的代碼中,我們在外層函數lazy_sum中又嵌套了的內層函數sumsum引用了lazy_sum的參數,並且lazy_sumsum 作爲返回結果。如果是按照命令式語言的規則(如C++,C#),在執行sum函數時,會由於在其作用域內找不到args變量而出錯,但是在函數式語言中,當內嵌的函數體內有引用外部作用域的變量時,將會把定義時涉及到的引用環境和函數體打包成一個整體返回,這中程序結構就是上面說的"閉包(Closure)"。所以說,閉包就是由函數及其相關的引用環境組合而成的實體,即:閉包=函數+引用環境。

從運行結果中可以看到,當調用lazy_sum(*args)時,返回的是內部求和函數的引用地址,當調用函數sum() 時,才真正計算求和的結果。

進一步理解Python中的閉包概念,還有兩點需要特別注意,首先看下面的例子:

s1 = lazy_sum(1, 2, 3, 4, 5)
s2 = lazy_sum(1, 2, 3, 4, 5)
print(s1==s2)

# Output>>>
False

也就是說當我們調用lazy_sum()時,即使傳入參數相同,每次調用也都會返回一個新的函數,彼此不會互相影響。

第二點,在閉包中修改外部作用域的局部變量時,需要使用關鍵字nonlocal,否則會報錯。稍微修改一下上面求和的代碼,如下:

# ax = 0  # UnboundLocalError
def lazy_sum(*args):
    ax = 0	# UnboundLocalError
    def sum():
        # ax = 0  # 正常
        for n in args:
            ax += n
        return ax

    return sum
  
s = lazy_sum(1, 2, 3, 4, 5)
print(s, type(s),s(),sep='\n')

# Output>>>
UnboundLocalError: local variable 'ax' referenced before assignment

我們嘗試將原本定義在內層函數中的變量 ax 放到了外層函數或函數體外部中進行定義,此時運行代碼都會拋出UnboundLocalError 的異常,這再看下面的代碼:

def decorator():
    name = "Rethink"
    def wrapper():
        print(name)
    return wrapper

deco()()

# Output>>>
Rethink

這裏看到,代碼可以正常運行,這是因爲在閉包中只是引用了外部作用域的局部變量,而沒有修改它的值。

在閉包結構中,內層函數改變外層函數的局部變量需要用nonlocal 關鍵字, nonlocal不能定義新的外層函數變量,只能改變已有的外層函數變量,同時也不能改變全局變量,在看下面的例子:

# ax = 0  # no binding for nonlocal 'ax' found
def lazy_sum(*args):
    ax = 0
    def sum():
        nonlocal ax
        for n in args:
            ax += n
        return ax
    return sum

s = lazy_sum(1, 2, 3, 4, 5)
print(s, type(s), s(), sep='\n')

# Output>>>
<function lazy_sum.<locals>.sum at 0x000001B0D80612F0>
<class 'function'>
15

從運行結果中可以看到,在閉包函數中使用 nonlocal 聲明外層函數中定義的ax變量後,程序可以正常運行,但是如果ax是定義在函數體外部的全局變量,則運行函數時,會報錯:no binding for nonlocal 'ax' found .

[To be continued…]

參考文檔

  1. Python基礎|深入閉包與變量作用域,公衆號:編程時光

  2. 深入理解Python變量作用域與函數閉包石曉文

  3. Python與算法社區,Emily

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章