python中的閉包函式

任平生78發表於2018-02-08

閉包函式初探

通常我們定義函式都是這樣定義的

def foo():
    pass

其實在函數語言程式設計中,函式裡面還可以巢狀函式,如下面這樣

def foo():
    print("hello world in foo")
    
    def bar():
        print("hello world in bar")

此時我們呼叫foo函式,執行結果會是什麼樣子的呢??

hello world in foo

結果如上所示,只會執行foo函式的第一層函式,bar函式是不會被執行的。為什麼呢

實際上來說,不管函式寫在哪個部分,那都只是定義了一個函式,只有這個函式被呼叫,函式內部的語句才會被執行

在上面的例子中,bar函式雖然在foo函式內部定義了,但是並沒有被執行,所以bar函式是不會被執行的

這樣說來,定義在一個函式內部的函式就沒什麼作用了嗎??其實不是這樣的。

來看下面的例子,把bar函式作為一個值返回給foo函式,來看執行過程

def foo():
    print("hello world in foo")
    
    def bar():
        print("hello world in bar")
    return bar

f1=foo()
print(f1)

此時,由於bar函式作為一個返回值被返回給了foo,所以foo函式執行結果是有返回值的

此時定義一個變數f1來接收foo函式的執行返回結果,然後列印f1

返回的結果如下

hello world in foo
<function foo.<locals>.bar at 0x0000000002941A60>

可以看到首先列印了foo函式中定義的一個print語句,接著列印的是foo函式中包含的bar函式的記憶體地址

既然是一個函式的記憶體地址,當然可以加括號來執行這個函式

def foo():
    print("hello world in foo")
    def bar():
        print("hello world in bar")
    return bar

f1=foo()
f1()

此時,這段程式碼的執行結果為:

hello world in foo
hello world in bar

兩個print語句都被列印出來了。

在上面的例子裡,首先定義了一個函式foo,接著在foo函式內部又巢狀定義了一個函式bar,然後返回函式bar的函式名,這就是閉包函式的定義方式。

其實,閉包的定義就是一個函式內部又巢狀了一個函式

來看下面的這段程式碼

    def foo():
        print("hello world in foo")
        name="python"
        def bar():
            print(name)
            print("hello world in bar")
        return bar
    
    f1=foo()
    f1()

在上面的例子裡,在外層函式中定義了一個變數name,然後在內層函式中列印這個變數name

此時執行上面的程式碼,在列印name這個變數的時候,會先在bar函式內部查詢name這個變數,但是bar函式裡面是沒有name這個變數的,

此時根據python查詢變數的LEGB法則,會到bar函式的外面一層去繼續查詢name這個變數,此時可以找到name這個變數

所以這裡列印的foo函式中定義的name的值

執行上面的程式碼,列印結果如下

hello world in foo
python
hello world in bar

這裡要記住很重要的一點就是:

內層函式引用了外層函式的區域性變數

來分析下上面的例子中程式的執行過程:

首先執行foo函式,foo函式的執行結果是返回bar的函式名,此時又把foo函式的執行結果定義給了變數f1,
所以此時f1就等於bar這個函式的記憶體地址,然後f1加括號執行就表示執行了bar函式。
在執行bar函式的過程中,bar函式訪問到了外層foo函式中定義的變數,這就是一個典型的閉包函式

那使用閉包函式有什麼好處呢??在上面的例子裡,f1的值是bar函式的記憶體地址,f1加括號執行就是在執行bar函式。

又由於f1是一個全域性變數,這意味著可以在整個程式的任意位置都可以執行f1函式,此時再定義一個函式,在這個函式內部呼叫f1函式,

    def foo():
        print("hello world in foo")
        name = "python"
    
        def bar():
            print(name)
            print("hello world in bar")
        return bar
    
    f1 = foo()
    
    def func():
        name = "aaaaa"
        f1()
    
    func()

來分析一下程式的執行過程:

1.執行func函式,程式會先在記憶體中申請一塊空間以儲存name變數的值,然後執行f1函式,f1是在全域性中定義的變數,所以一定可以找到f1函式的記憶體地址
2.f1加括號執行,就是在執行一個閉包函式,這個閉包函式內部引用了name這個變數
3.name這個變數在bar函式的外部已經定義了,所以在func函式內部呼叫f1函式,也就是bar函式時,其引用的變數依然是foo函式內部定義的name變數,而不是func函式內部定義的name變數,
4.因為f1函式的內部已經包含了name這個函式的值,所以就算在func函式內部也定義了name這個變數,程式執行的結果列印的依然是foo函式內部定義的name的值

程式執行結果

hello world in foo
python
hello world in bar

怎樣驗證一個函式是閉包函式

首先,閉包函式都有一個特有的屬性:closure

在上面的例子裡,列印f1的__closure__屬性

    def foo():
        name = "python"
    
        def bar():
            print(name)
            print("hello world in bar")
        return bar
    
    f1 = foo()
    print(f1.__closure__)

列印結果如下:

(<cell at 0x0000000001DF5708: str object at 0x0000000001E79688>,)

可以看到__closure__屬性的列印結果是一個元組形式的,其值就是f1函式的外層函式作用域

此時可以呼叫__closure__返回的元組的元素的cell_contents方法列印出name變數的值

    def foo():
        name = "python"
    
        def bar():
            print(name)
            print("hello world in bar")
        return bar
    
    f1 = foo()
    print(f1.__closure__[0].cell_contents)

列印結果如下:

python

可以看到程式已經列印出name變數的值了

即然__closure__的返回結果是一個元組,那麼這個元組中一定是可以包含多個值的,看下面的例子

在foo函式內部定義多個變數,然後在bar函式內部列印幾個變數的值,

然後執行這個閉包函式,列印閉包函式的__closure__方法

    def foo():
        print("hello world in foo")
        name1 = "python1"
        name2 = "python2"
        name3 = "python3"
        name4 = "python4"
    
        def bar():
            print(name1)
            print(name2)
            print(name3)
            print(name4)
            print("hello world in bar")
        return bar
    
    f1 = foo()
    print(f1.__closure__)

程式執行結果

(<cell at 0x0000000002145708: str object at 0x00000000021C9260>, 
<cell at 0x0000000002145A08: str object at 0x00000000021C93B0>, 
<cell at 0x0000000002145768: str object at 0x000000000295BE30>, 
<cell at 0x0000000002145C18: str object at 0x0000000002963880>)

由於在foo函式內部定義了4個變數,而且在bar函式內部引用了這4個變數,所以列印這個閉包函式的__closure__方法,返回的元組中就有4個元素

現在可以分別列印返回的元組中的這4個字串物件的值了

    def foo():
        name1 = "python1"
        name2 = "python2"
        name3 = "python3"
        name4 = "python4"
    
        def bar():
            print(name1)
            print(name2)
            print(name3)
            print(name4)
            print("hello world in bar")
        return bar
    
    f1 = foo()
    print(f1.__closure__[0].cell_contents)
    print(f1.__closure__[1].cell_contents)
    print(f1.__closure__[2].cell_contents)
    print(f1.__closure__[3].cell_contents)

程式執行結果

python1
python2
python3
python4

那麼現在還剩下最後一個問題了,那就是閉包函式的內層函式一定要返回嗎??

來看下面一個例子

    def foo():
        name = "python1"
    
        def bar():
            print(name)
        print(bar.__closure__)
    
    foo()

定義了一個巢狀函式,然後這個巢狀函式的內層函式沒有被返回,而是直接列印內層函式的__closure__方法,然後直接呼叫外層函式。

程式執行結果

(<cell at 0x0000000002155708: str object at 0x00000000021D9688>,)

依然列印出了內層函式的引用的變數物件

這說明閉包函式的內層函式還一定要返回

閉包函式的內層函式可以呼叫全域性變數嗎??

把外層函式內部定義的變數改為全域性變數,然後在內層函式中引用這個變數

    name = "python1"
    
    def foo():
        def bar():
            print(name)
    
        print(bar.__closure__)
    
    f=foo()
    print(f)

程式執行結果

None
None

可以看到,程式的執行結果是兩個None,巢狀函式的內層函式的__closure__函式的值為None

這說明foo函式的內層巢狀函式bar呼叫的全域性變數沒有成功,所以上面的例子不是一個閉包函式

關於閉包函式的一些總結:

閉包的定義為:
    在函式內部定義的函式,稱為內部函式
    內部函式呼叫了外部函式的區域性變數
    即使內部函式返回了,還是可以使用區域性變數
    通常閉包函式的內層函式都要被返回給外部函式
    閉包函式的外部函式可以在任何地方被呼叫,而不再受函式定義時層級的限制

閉包函式的作用

1.閉包函式自帶函式作用域

正常意義上的函式,在函式執行過程中查詢變數的順序是一層一層向外找,符合LEGB(Local->Enclose->Global->Built in)法則的,

但是對閉包函式來說,查詢變數只會找內部函式外面的那一層,因為閉包函式本身就自帶一層作用域,這樣才符合”閉包”兩個字的意思

2.延遲計算(也叫惰性計算)

看下面的例子

    def func():
        name="python"
        def bar():
            print(name)
        return bar
    
    f=func()
    print(f.__closure)

在上面的例子裡,執行foo()函式的返回結果是一個包含自帶的某種狀態的函式,實際上這個函式並沒有執行,

以後想執行這個自帶狀態的函式時,把func()返回結果所賦值的那個變數加括號就可以執行了,

3.要想讓一個函式始終保持一種狀態,就可以使用閉包

例子:

    name="python"
    
    def func():
        print("I like %s" % name)
    
    func()

上面的程式碼執行結果會列印一行:”I like python”

但是我們知道,在不同的地方呼叫func函式,列印的結果很大可能是不一樣的

那麼如果我想不管在什麼地方呼叫func函式,列印的結果都是”I like python”時,

就可以使用閉包了。

    def func1():
    
        name="python"
        def func():
            print("I like %s" % name)
        return func
    
    func=func1()
    func()

如上圖所示,在func函式外面再包含一層函式func1,執行func1函式,再把func1函式的返回結果賦值給func這個變數

此時func就是一個閉包函式了,把func函式加括號就可以執行了

而且我們一定知道,此時func函式的執行結果一定會列印”I like python”這句話,而且不管func函式在程式的哪個位置被呼叫,執行結果都是一樣的


相關文章