返回函數,顧名思義,就是高階函數可以把函數作爲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()
結果應該是1
,4
,9
,但實際結果是:
>>> 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