零基礎學習 Python 之閉包

Python空間發表於2019-03-04

寫在之前

在正式講「閉包」之前我們首先得先知道「巢狀函式」這麼一個東西,我在之前的文章中(零基礎學習 Python 之函式物件)說過,函式不單單可以作為物件來傳遞,還可以在一個函式裡面巢狀一個函式,這個就是我們今天要講的巢狀函式。

巢狀函式

首先我們來看一個例子:

>>> def my_name():
...    def your_name():
...            print('your_name() is two dog')
...    print('my_name() is rocky')
...
複製程式碼

上面就是一個簡單的巢狀函式的例子,在上面的程式碼中,在函式 my_name() 中定義了函式 your_name(),而 your_name() 就稱為 my_name() 的「內嵌函式」,因為它是在 my_name() 裡面的定義。

然後我們來呼叫 my_name(),會得到下面的結果:

>>> my_name()
my_name() is rocky
複製程式碼

這個結果說明在上面的呼叫方式和內嵌函式的寫法中,your_name() 這個函式根本沒被呼叫,或者我們可以這麼說,那就是 my_name() 沒有按照從上到下的順序依次執行其裡面的程式碼。

那麼我想要 your_name() 這個內嵌函式也執行,該怎麼做呢?其實在 my_name() 裡面顯示的呼叫一下 your_name() 函式就好了,請看下面的程式碼:

>>> def my_name():
...    def your_name():
...            print('your_name() is two dog')
...    your_name() #顯示的呼叫內嵌函式
...    print('my_name() is rocky')
...
複製程式碼

我們現在來呼叫 my_name(),執行結果如下:

>>> my_name()
your_name() is two dog
my_name() is rocky
複製程式碼

現在我們再來思考一個問題,我們能不能在 my_name() 外面單獨的呼叫其內嵌函式 your_name() 呢?我們來試一下:

>> your_name()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
NameError: name 'your_name' is not defined
複製程式碼

結果會顯示錯誤資訊,這說明這樣呼叫是不行的,原因就是 your_name() 是定義在 my_name() 裡面的函式,它生效的範圍僅限於 my_name() 函式體之內,也就是說它的作用域就是 my_name() 的範圍而已,既然是這樣,那麼 your_name 在使用變數的時候也就會收到 my_name() 的約束。

我們再來看一個例子:

>>> def fun1():
...    a = 1
...    def fun2():
...            a += 1
...            print('fun2 -- a = ',a)
...    fun2()
...    print('fun1 -- a = ',a)
...
複製程式碼

在看下面的結果之前,請你想一想這個函式的結果會是什麼?加入你思考完畢,請看下面的結果:

>>> fun1()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 6, in fun1
 File "<stdin>", line 4, in fun2
UnboundLocalError: local variable 'a' referenced before assignment
複製程式碼

你猜對了麼?結果是執行錯誤!我們觀察報錯的資訊,原因是 fun2() 裡面使用了 fun1() 的變數 a,按照表示式, Python 直譯器認為這個變數應該在 fun2() 中建立,而不是引用 fun1() 中的變數,所以才報錯。

在 Python 中,我們可以使用 nonlocal 這個關鍵詞,具體操作見下例:

>>> def fun1():
...    a = 1
...    def fun2():
...   nonlocal a
...            a += 1
...            print('fun2 -- a = ',a)
...    fun2()
...    print('fun1 -- a = ',a)
...
複製程式碼

然後我們呼叫 fun1() 函式,得到如下結果:

fun2 -- a = 2
fun1 -- a = 2
複製程式碼

綜上所述就是巢狀函式的原理,剩下的就是在實踐中去運用它,達到加深理解的目的。

這個巢狀函式,其實可以製作動態的函式物件,而這個話題延伸下去,就是所謂的「閉包」。

閉包

我們都知道在數學中有閉包的概念,但此處我要說的閉包是計算機程式語言中的概念,它被廣泛的使用於函數語言程式設計。

關於閉包的概念,官方的定義頗為嚴格,也很難理解,在《Python語言及其應用》一書中關於閉包的解釋我覺得比較好 -- 閉包是一個可以由另一個函式動態生成的函式,並且可以改變和儲存函式外建立的變數的值。乍一看,好像還是比較很難懂,下面我用一個簡單的例子來解釋一下:

>>> a = 1
>>> def fun():
...     print(a)
...
>>> fun()
1
>>> def fun1():
...     b = 1
...
>>> print(b)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
NameError: name 'b' is not defined
複製程式碼

毋庸置疑,第一段程式是可以執行成功的,a = 1 定義的變數在函式裡可以被呼叫,但是反過來,第二段程式則出現了報錯。

在函式 fun() 裡可以直接使用外面的 a = 1,但是在函式 fun1() 外面不能使用它裡面所定義的 b = 1,如果我們根據作用域的關係來解釋,是沒有什麼異議的,但是如果在某種特殊情況下,我們必須要在函式外面使用函式裡面的變數,該怎麼辦呢?

我們先來看下面的例子:

>>> def fun():
...    a = 1
...    def fun1():
...            return a
...    return fun1
...
>>> f = fun()
>>> print(f())
1
複製程式碼

如果你仔細看過上面文章的內容,你一定覺得的很眼熟,上述的本質就是我們所講的巢狀函式。

在函式 fun() 裡面,有 a = 1 和 函式 fun1() ,它們兩個都在函式 fun() 的環境裡面,但是它們兩個是互不干擾的,所以 a 相對於 fun1() 來說是自由變數,並且在函式 fun1() 中應用了這個自由變數 -- 這個 fun1() 就是我們所定義的閉包。

閉包實際上就是一個函式,但是這個函式要具有

  • 1.定義在另外一個函式裡面(巢狀函式);
  • 2.引用其所在環境的自由變數。

上述例子通過閉包在 fun() 執行完畢時,a = 1依然可以在 f() 中,即 fun1() 函式中存在,並沒有被收回,所以 print(f()) 才得到了結果。

當我們在某些時候需要對事務做更高層次的抽象,用閉包會相當舒服。比如我們要寫一個二元一次函式,如果不使用閉包的話相信你可以輕而易舉的寫出來,下面讓我們來用閉包的方式完成這個一元二次方程:

>>> def fun(a,b,c):
...    def para(x):
...            return a*x**2 + b*x + c
...    return para
...
>>> f = fun(1,2,3)
>>> print(f(2))
11
複製程式碼

上面的函式中,f = fun(1,2,3) 定義了一個一元二次函式的函式物件,x^2 + 2x + 3,如果要計算 x = 2 ,該一元二次函式的值,只需要計算 f(2) 即可,這種寫法是不是看起來更簡潔一些?

當我們在後面學習了類的知識以後,再回過頭來看閉包的應用,其實你會有更深的認識,這個我們在後面再做討論,先知道有類這麼一個概念就好了。

寫在之後

當然閉包在實際的應用中還有很多方面,作為零基礎入門這個系列我們就到此為止,不做深究,可能在後面我會在別的系列中再進一步的講一下,如果你現在對這個方面很感興趣,可以 Google 一下這方面的文章,有很多的。

更多內容,歡迎關注公眾號「Python空間」,期待和你的交流。

在這裡插入圖片描述

相關文章