python高階函數 閉包

高階函數 Higher-order function

數學計算機科學中,高階函數是至少滿足下列一個條件的函數

  • 接受一個或多個函數作爲輸入
  • 輸出一個函數

函數作爲輸入:

一個最簡單的高階函數:

def add(x, y, f):
    return f(x) + f(y)

當我們調用add(-5, 6, abs)時,參數xyf分別接收-56abs 

函數作爲輸出

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)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>

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

>>> f()
25

在這個例子中,我們在函數lazy_sum中又定義了函數sum,並且,內部函數sum可以引用外部函數lazy_sum的參數和局部變量,當lazy_sum返回函數sum時,相關參數和變量都保存在返回的函數中,這種稱爲“閉包(Closure)”的程序結構擁有極大的威力。

 

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

>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False

f1()f2()的調用結果互不影響。

閉包 Closure

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

另一個需要注意的問題是,返回的函數並沒有立刻執行,而是直到調用了f()才執行。我們來看一個例子:

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

 深入理解閉包

什麼是閉包?閉包有什麼用?爲什麼要用閉包?今天我們就帶着這3個問題來一步一步認識閉包。閉包和函數緊密聯繫在一起,介紹閉包前有必要先介紹一些背景知識,諸如嵌套函數、變量的作用域等概念

作用域

作用域是程序運行時變量可被訪問的範圍,定義在函數內的變量是局部變量,局部變量的作用範圍只能是函數內部範圍內,它不能在函數外引用。

def foo():
    num = 10 # 局部變量
print(num)  # NameError: name 'num' is not defined

定義在模塊最外層的變量是全局變量,它是全局範圍內可見的,當然在函數裏面也可以讀取到全局變量的。例如:

num = 10 # 全局變量
def foo():
    print(num)  # 10

嵌套函數

函數不僅可以定義在模塊的最外層,還可以定義在另外一個函數的內部,像這種定義在函數裏面的函數稱之爲嵌套函數(nested function)例如:

def print_msg():
    # print_msg 是外圍函數
    msg = "zen of python"

    def printer():
        # printer是嵌套函數
        print(msg)
    printer()
# 輸出 zen of python
print_msg()

對於嵌套函數,它可以訪問到其外層作用域中聲明的非局部(non-local)變量,比如代碼示例中的變量 msg 可以被嵌套函數 printer 正常訪問。

那麼有沒有一種可能即使脫離了函數本身的作用範圍,局部變量還可以被訪問得到呢?答案是閉包

什麼是閉包

函數身爲第一類對象,它可以作爲函數的返回值返回,現在我們來考慮如下的例子:

def print_msg():
    # print_msg 是外圍函數
    msg = "zen of python"
    def printer():
        # printer 是嵌套函數
        print(msg)
    return printer

another = print_msg()
# 輸出 zen of python
another()

這段代碼和前面例子的效果完全一樣,同樣輸出 "zen of python"。不同的地方在於內部函數 printer 直接作爲返回值返回了。

一般情況下,函數中的局部變量僅在函數的執行期間可用,一旦 print_msg() 執行過後,我們會認爲 msg變量將不再可用。然而,在這裏我們發現 print_msg 執行完之後,在調用 another 的時候 msg 變量的值正常輸出了,這就是閉包的作用,閉包使得局部變量在函數外被訪問成爲可能。

看完這個例子,我們再來定義閉包,維基百科上的解釋是:

在計算機科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。

這裏的 another 就是一個閉包,閉包本質上是一個函數,它有兩部分組成,printer 函數和變量 msg。閉包使得這些變量的值始終保存在內存中。

閉包,顧名思義,就是一個封閉的包裹,裏面包裹着自由變量,就像在類裏面定義的屬性值一樣,自由變量的可見範圍隨同包裹,哪裏可以訪問到這個包裹,哪裏就可以訪問到這個自由變量。

爲什麼要使用閉包

閉包避免了使用全局變量,此外,閉包允許將函數與其所操作的某些數據(環境)關連起來。這一點與面向對象編程是非常類似的,在面對象編程中,對象允許我們將某些數據(對象的屬性)與一個或者多個方法相關聯。

一般來說,當對象中只有一個方法時,這時使用閉包是更好的選擇。來看一個例子:

def adder(x):
    def wrapper(y):
        return x + y
    return wrapper

adder5 = adder(5)
# 輸出 15
adder5(10)
# 輸出 11
adder5(6)

這比用類來實現更優雅,此外裝飾器也是基於閉包的一中應用場景。

所有函數都有一個 __closure__屬性,如果這個函數是一個閉包的話,那麼它返回的是一個由 cell 對象 組成的元組對象。cell 對象的cell_contents 屬性就是閉包中的自由變量。

>>> adder.__closure__
>>> adder5.__closure__
(<cell at 0x103075910: int object at 0x7fd251604518>,)
>>> adder5.__closure__[0].cell_contents
5

這解釋了爲什麼局部變量脫離函數之後,還可以在函數之外被訪問的原因的,因爲它存儲在了閉包的 cell_contents中了。

練習

利用閉包返回一個計數器函數,每次調用它返回遞增整數:

def createCounter():
    i = 0
    def counter():
        nonlocal i
        i += 1
        return i
    return counter

# 測試:
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
    print('測試通過!')
else:
    print('測試失敗!')

 

https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001431835236741e42daf5af6514f1a8917b8aaadff31bf000#0

https://foofish.net/python-closure.html

https://segmentfault.com/a/1190000004461404#articleHeader0

 

什麼是閉包


官方解釋(譯文)

Go 函數可以是一個閉包。閉包是一個函數值,它引用了函數體之外的變量。 這個函數可以對這個引用的變量進行訪問和賦值;換句話說這個函數被“綁定”在這個變量上。

例如,函數 adder 返回一個閉包。每個返回的閉包都被綁定到其各自的 sum 變量上。

在上面例子中(這裏重新貼下代碼,和上面代碼一樣):

package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

上面背景高亮部分就是一個閉包,如pos := adder()的adder()表示返回了一個閉包,並賦值給了pos,同時,這個被賦值給了pos的閉包函數被綁定在sum變量上,因此pos閉包函數裏的變量sum和neg變量裏的sum毫無關係。

Note

func adder() func(int) intfunc(int) int表示adder()的輸出值的類型是func(int) int這樣一個函數

我對閉包的理解


沒有閉包的時候,函數就是一次性買賣,函數執行完畢後就無法再更改函數中變量的值(應該是內存釋放了);有了閉包後函數就成爲了一個變量的值,只要變量沒被釋放,函數就會一直處於存活並獨享的狀態,因此可以後期更改函數中變量的值(因爲這樣就不會被go給回收內存了,會一直緩存在那裏)。

比如,實現一個計算功能:一個數從0開始,每次加上自己的值和當前循環次數(當前第幾次,循環從0開始,到9,共10次),然後*2,這樣迭代10次:

沒有閉包的時候這麼寫:

func abc(x int) int {
    return x * 2
}

func main() {
    var a int
    for i := 0; i < 10; i ++ {
        a = abc(a+i)
        fmt.Println(a)
    }
}

如果用閉包可以這麼寫:

func abc() func(int) int {
    res := 0
    return func(x int) int {
        res = (res + x) * 2
        return res
    }
}

func main() {
    a := abc()
    for i := 0; i < 10; i++ {
        fmt.Println(a(i))
    }
}

2種寫法輸出值都是:

0
2
8
22
52
114
240
494
1004
2026

從上面例子可以看出閉包的3個好處:

  1. 不是一次性消費,被引用聲明後可以重複調用,同時變量又只限定在函數裏,同時每次調用不是從初始值開始(函數里長期存儲變量)

    這有點像使用面向對象的感覺,實例化一個類,這樣這個類裏的所有方法、屬性都是爲某個人私有獨享的。但比面向對象更加的輕量化

  2. 用了閉包後,主函數就變得簡單了,把算法封裝在一個函數裏,使得主函數省略了a=abc(a+i)這種麻煩事了

  3. 變量污染少,因爲如果沒用閉包,就會爲了傳遞值到函數裏,而在函數外部聲明變量,但這樣聲明的變量又會被下面的其他函數或代碼誤改。

關於閉包的第一個好處,再囉嗦舉個例子

  1. 若不用閉包,則容易對函數外的變量誤操作(誤操作別人),例:

    var A int = 1
    func main() {
        foo := func () {
            A := 2
            fmt.Println(A)
        }
        foo()
        fmt.Println(A)
    }
    

    輸出:

    2
    1
    

    如果手誤將A := 2寫成了A = 2,那麼輸出就是:

    2
    2
    

    即會影響外部變量A

  2. 爲了將某一個私有的值傳遞到某個函數裏,就需要在函數外聲明這個值,但是這樣聲明會導致這個值在其他函數裏也可見了(別人誤操作我),例:

    func main() {
        A := 1
        foo := func () int {
            return A + 1
        }
        B := 1
        bar := func () int {
            return B + 2
        }
        fmt.Println(foo())
        fmt.Println(bar())
    }
    

    輸出:

    2
    3
    

    在bar裏是可以對變量A做操作的,一個不小心就容易誤修改變量A

    結論:函數外的變量只能通過參數傳遞進去,不要通過全局變量的方式的渠道傳遞進去,當函數內能讀取到的變量越多,出錯概率(誤操作)也就越高。

最後舉個例子


實現斐波那契數列:

用閉包:

func fibonacci() func() int {
    b1 := 1
    b2 := 0
    bc := 0
    return func() int {
        bc = b1 + b2
        b1 = b2
        b2 = bc
        return bc
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
    }
}

輸出

1
1
2
3
5
8
13
21
34
55

不用閉包:

func fibonacci(num int) {
    b1 := 1
    b2 := 0
    bc := 0

    for i := 0; i < num; i ++ {
        bc = b1 + b2
        b1 = b2
        b2 = bc
        fmt.Println(bc)
    }
}

func main() {
    fibonacci(10)
}

這樣輸出也是正確的,但是這麼寫的話就將循環次數交給了fibonacci函數,即這個函數是個一次性使用的,當函數執行完畢後如果再執行函數,又是從初始值(這裏是b1=1)開始,如果想能繼續下去,就必須在函數外聲明變量,但這樣又造成了變量的泛濫(即對其他代碼來說這幾個變量是毫無意義,還可能造成對這幾個變量的誤操作),而有的時候想把for循環交由main控制,而是讓fibonacci函數完成核心算法、核心數據存儲,同時變量又不氾濫給其他代碼

不用閉包也可以這麼寫:

func main() {
    b1 := 1
    b2 := 0
    bc := 0
    fibonacci := func () {
        bc = b1 + b2
        b1 = b2
        b2 = bc
        fmt.Println(bc)
    }
    for i := 0; i < 10; i ++ {
        fibonacci()
    }
}

這樣輸出結果也是相同的,但是這麼寫的話,b1、b2、bc就變成了全局都能引用的變量了,而這3個變量其實只在fibonacci裏用的到,所以這樣把b1、b2、bc給放到全局就顯得毫無意義,還有可能對這3個變量誤操作

後退

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