理解Python函式閉包

c-xuan發表於2019-03-29

本文主要介紹什麼是閉包,Ptyhon中使用閉包時容易出現的變數問題。

閉包

閉包指延伸了作用域的函式,其中包含函式定義體中引用、但是不在定義體中定義的非全域性變數。函式是不是匿名的沒有關係,關鍵是它能訪問定義體之外定義的非全域性變數。

舉個栗子

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

avg = make_averager()
avg(10) # 10.0
avg(13) # 11.5
avg(19) # 14.0

上面定義了一個巢狀函式,作用是計算移動平均值。
呼叫make_averager時,返回一個averager函式物件。每次呼叫averager時,會把一個新的引數值新增到列表中,然後計算列表的平均值。seriesmake_averager函式的區域性變數,但是呼叫avg(10)時,make_averager()函式已經返回了,所以它的本地作用域也沒有了。

averager()函式中,series變成了自由變數,就是沒在本地作用域中繫結的變數。

在這裡插入圖片描述

所以閉包是一種函式,它會保留定義函式時存在的自由變數的繫結,在呼叫函式時,雖然定義作用域不可用了但是還可以使用繫結的變數。注意,只有巢狀在其他函式中的函式才可能需要處理不在全域性作用域中的外部變數

栗子改進

雖然上面函式可以實現計算移動平均,但是效率不高,因為把所有歷史資料都保留在列表中。如果只儲存總和以及元素個數,再求平均值更好些。

def make_averager():
    count = 0
    total = 0
    
    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    
    return averager

上述函式在呼叫averager()時是有問題的,原因是count初始定義是數字,是不可變型別。在執行count = count + 1 時,count變成了區域性變數,不是自由變數,所以解析器認為countaverager()裡沒有定義。total變數也是這樣的問題。

問題顯然出現在變數是否是可變型別上,在用列表計算的時候,採用的series是可變的列表型別。但是對於數字、字串、元祖等不可變型別,巢狀函式內只能讀取,不能更新,如果嘗試重新繫結,比如count = count + 1,就會隱式建立區域性變數,不會成為自由變數儲存在閉包裡。

python3下解決方式是引入nonlocal宣告。作用就是把變數標記為自由變數。

def make_averager():
    count = 0
    total = 0
    
    def averager(new_value):
        nonlocal count,total
        count += 1
        total += new_value
        return total / count
    
    return averager

總結

在函式內巢狀的函式如果引用了外層函式定義的變數,外部呼叫巢狀的函式時,可以認為這時候函式變數的作用域延伸了,存在自由變數。主意自由變數如果是不可變型別,需要使用nonlocal宣告。那麼閉包有什麼用呢?如果要了解Python裝飾器,或許會有所幫助。

參考Luciano Ramalho-《Fluent Python》

更多

相關文章