Python中的返回函數與閉包

返回函數,顧名思義,就是高階函數可以把函數作爲return值返回。與閉包的關係是:閉包需要以返回函數的形式實現。

 

一. 返回函數

比如我們有一個求和函數:

>>> def calc_sum(num_list):
    s = 0
    for i in num_list:
        s += i
    return s

>>> calc_sum([1,2,3,4])
10

當我們不需要立刻求和,而是後面根據需要再計算結果時,我們可以返回求和的函數,而不是直接返回計算結果。這就是返回函數。

>>> def lazy_calc_sum(num_list):
    def calc_sum():
        s = 0
        for i in num_list:
            s += i
        return s
    return calc_sum

>>> f_lazy = lazy_calc_sum([1,2,3,4])
>>> f_lazy
<function lazy_calc_sum.<locals>.calc_sum at 0x0000003A8D92E9D8>
>>> f_lazy()
10

很顯然,這樣能讓我們根據需求,節省計算資源。

 

二. 閉包

在上面的例子中,我們在函數lazy_clac_sum中又定義了函數calc_sum,並且,內部函數calc_sum可以引用外部函數lazy_calc_sum的參數和局部變量,當lazy_calc_sum返回函數calc_sum時,相關參數和變量都保存在返回的函數中,這種稱爲“閉包(Closure)”。

如果讓定義更加清晰一些: 如果在一個內部函數裏對在外部作用域(但不是在全局作用域)的變量進行引用,但不在全局作用域裏,則這個內部函數就是一個閉包。

實際上,閉包的用處/優點有兩條:

  • 從函數外可以讀取函數內部的變量
  • 讓這些變量的值始終保持在內存中(也可以理解爲保留當前運行環境)

 下面例子是,我們創建了一個下載download函數,然後下載次數一直存儲在內存中。

>>> def download_enter(download_times):
    def download():
        download_times += 1
        print("This is the %s time download" % download_times)
    return download

>>> 
>>> d = download_enter(0)
>>> d()
This is the 1 time download
>>> d()
This is the 2 time download
>>> d()
This is the 3 time download

 

下面的例子是閉包根據外部作用域的局部變量來得到不同的結果,這有點像一種類似配置功能的作用,我們可以修改外部的變量,閉包根據這個變量展現出不同的功能。比如有時我們需要對某些文件的特殊行進行分析,先要提取出這些特殊行。

def make_filter(keep):  
    def the_filter(file_name):  
        file = open(file_name)  
        lines = file.readlines()  
        file.close()  
        filter_doc = [i for i in lines if keep in i]  
        return filter_doc  
    return the_filter  

如果我們需要取得文件"result.txt"中含有"pass"關鍵字的行,則可以這樣使用例子程序

filter = make_filter("pass")  
filter_result = filter("result.txt") 

以上兩種使用場景,用面向對象也是可以很簡單的實現的,但是在用Python進行函數式編程時,閉包對數據的持久化以及按配置產生不同的功能,是很有幫助的。

 

關於閉包的2個常見錯誤:

1. 嘗試在閉包中改變外部作用域的局部變量

def foo():  
    a = 1  
    def bar():  
        a = a + 1  
        return a  
    return bar  
>>> c = foo()  
>>> print c()  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
  File "<stdin>", line 4, in bar  
UnboundLocalError: local variable 'a' referenced before assignment  

這段程序的本意是要通過在每次調用閉包函數時都對變量a進行遞增的操作。但在實際使用時,a = a + 1的a會被python解釋器認爲是bar()函數的局部變量,從而引起“referenced before assignment”的錯誤。

解決方法有兩個:

  方法一:將a設置爲一個容器,比如列表List (不推薦)

  方法二:將a聲明爲nonlocal變量(僅在Python3支持),這樣聲明過後,就不會被認爲是bar()函數的局部變量,而是會到上一層函數環境中尋找這個變量。

下面是例子:

>>> def foo():
    a = 1
    b = [1]
    def bar():
        nonlocal a
        a = a + 1
        b[0] = b[0] + 1
        return a,b[0]
    return bar

>>> c = foo()
>>> print(c())
(2, 2)
>>> print(c())
(3, 3)
>>> print(c())
(4, 4)

 

2. 誤以爲返回的函數就已執行,對執行結果誤判

直接舉例子說明:

def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()

在上面的例子中,每次循環,都創建了一個新的函數,然後,把創建的3個函數都返回了。

你可能認爲調用f1()f2()f3()結果應該是149,但實際結果是:

>>> f1()
9
>>> f2()
9
>>> f3()
9

全部都是9!原因就在於返回的函數引用了變量i,但它並非立刻執行。等到3個函數都返回時,它們所引用的變量i已經變成了3,因此最終結果爲9

返回閉包時牢記一點:返回函數不要引用任何循環變量,或者後續會發生變化的變量。

如果一定要引用循環變量怎麼辦?方法是再創建一個函數,用該函數的參數綁定循環變量當前的值,無論該循環變量後續如何更改,已綁定到函數參數的值不變:

def count():
    def f(j):
        def g():
            return j*j
        return g
    fs = []
    for i in range(1, 4):
        fs.append(f(i)) # f(i)立刻被執行,因此i的當前值被傳入f()
    return fs

結果是:

>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9

 

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