天天看點

Python的傳回函數——函數作為傳回值 和 閉包

傳回函數

高階函數除了可以接受函數作為參數外,還可以把函數作為結果值傳回。

例如:實作一個可變參數的求和。通常情況下,求和的函數是這樣定義的:

def calc_sum(*args):  # calc ['kælk] 計算; calculate
    ax = 0
    for n in args:
        ax = ax + n
    return ax
print(calc_sum(1, 3, 5, 7, 9))

# 或者
def calc_sum(lst):
    return sum(lst)  # sum(iterable, start=0, /)
print(calc_sum([1, 3, 5, 7, 9]))
           

但是,如果不需要立刻求和,而是在後面的代碼中,根據需要再計算怎麼辦?可以不傳回求和的結果,而是傳回求和的函數,即傳回函數可以把一些計算延遲執行:

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

當我們調用 lazy_sum()時,傳回的并不是求和結果,而是求和函數:

f = lazy_sum(1, 3, 5, 7, 9)
print(f)
# 輸出結果
<function lazy_sum.<locals>.sum at 0x00000227705EC268>
           

調用函數 f 時,才真正計算求和的結果:

print(f())
#輸出結果
25
           

注:(1) 在這個例子中,我們在函數 lazy_sum 中又定義了函數 sum,并且,内部函數 sum 可以引用外部函數 lazy_sum 的參數和局部變量,當 lazy_sum 傳回函數 sum 時,相關參數和變量都儲存在傳回的函數中,這種稱為“閉包(Closure) ”的程式結構擁有極大的威力。

(2) 當我們調用 lazy_sum()時,每次調用都會傳回一個新的函數,即使傳入相同的參數:

f1= lazy_sum(1, 3, 5, 7, 9)

f2= lazy_sum(1, 3, 5, 7, 9)

print(f1 == f2)

#輸出結果

False

執行個體1.  請編寫一個函數calc_prod(lst),它接收一個list,傳回一個函數,傳回函數可以計算參數的乘積。

from functools import reduce #從functools子產品引入reduce()函數
def calc_prod(lst): # product 乘積;産品
    def lazy_prod():
        def f(x,y):
            return x * y
        return reduce(f, lst)
    return lazy_prod
f = calc_prod([1, 2, 3, 4])
print (f())
#輸出結果
24
           

閉包

注意到傳回的函數在其定義内部引用了局部變量 args,是以,當一個函數傳回了一個函數後,其内部的局部變量還被新函數引用,是以,閉包用起來簡單,實作起來可不容易。

另一個需要注意的問題是,傳回的函數并沒有立刻執行,而是直到調用

了 f()才執行。我們來看一個例子:

# why f1(), f2(), f3() returns 9, 9, 9 rather than 1, 4, 9?
def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i * i
        fs.append(f)
    return fs

f1, f2, f3 = count()  # count()執行了函數,函數傳回了fs,fs是一個由三個函數組成的清單,即
print(f1)             # fs = [f(i),f(i),f(i)],這個時候的f1,f2,f3隻是作為變量給這些函
print(f2)             # 數一個名字而已。
print(f3)
#輸出結果
<function count.<locals>.f at 0x0000024878ECC268>
<function count.<locals>.f at 0x0000024878ECC268>
<function count.<locals>.f at 0x0000024878ECC268>
print(f1())
print(f2())
print(f3())
#輸出結果
9
9
9
           

注:全部都是 9!原因就在于傳回的函數引用了變量 i,但它并非立刻執行。等到 3 個函數都傳回時,它們所引用的變量 i 已經變成了 3,是以最終結果為 9。

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

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

# fix:
def count():
    fs = []
    def f(n):
        def j():
            return n * n
        return j
    for i in range(1, 4):
        fs.append(f(i))  # f(i)立刻被執行,是以 i 的目前值被傳入 f()
    return fs

f1, f2, f3 = count()

print(f1)  
print(f2)
print(f3)
#輸出結果
<function count.<locals>.f.<locals>.j at 0x0000029404F48E18>
<function count.<locals>.f.<locals>.j at 0x0000029404F48EA0>
<function count.<locals>.f.<locals>.j at 0x0000029404F48F28>

print(f1())
print(f2())
print(f3())
#輸出結果
1
4
9
           

注:缺點是代碼較長,可利用 lambda 函數縮短代碼。(待完成)