1. 程式人生 >基礎教學 >Python入門教學 >24 Python 的閉包簡介

24 Python 的閉包簡介


閉包是較難理解的概念,Python 初學者可以暫時跳過此節。學習此節時需要理解 “函式是第一類物件” 的概念,在詞條 “Python 的 lambda 表示式” 中詳細介紹了這一概念。

本節首先講解理解閉包所需要的鋪墊知識,最後再引入閉包的定義。

1. 巢狀定義函式

1.1 在函式內部定義函式

Python 允許巢狀定義函式,可以在函式中定義函式,例如:

def outter():
    def inner():
        print('Inside inner')

    print('Inside outter')
    inner()

outter()    
  • 在第 1 行,定義函式 outter
  • 在第 2 行,在函式 outter 內部,定義函式 inner
  • 在第 6 行,在函式 outter 內部,呼叫函式 inner

函式 inner 定義在函式 outter 中,被稱為函式巢狀定義。執行程式,輸出結果如下:

Inside outter
Inside inner

1.2 實現資訊隱藏

定義在函式內部的函式,對外是不可見的,例如:

def outter():
    def inner():
        print('inside inner')

    print('inside outter')        
    inner()

inner()    
  • 在第 1 行,定義了外部函式 outter
  • 在第 2 行,定義了內部函式 inner
  • 在第 6 行,在函式 outter 中,呼叫函式 inner
  • 在第 8 行,呼叫函式 inner

程式執行,輸出如下:

Traceback (most recent call last):
  File "visible.py", line 8, in <module>
    inner()
NameError: name 'inner' is not defined

在第 4 行,試圖呼叫定義在函式 outter 內部定義的函式 inner,程式執行時報錯:name ‘inner’ is not defined,即找不到函式 inner。

因為函式 inner 是定義在函式 outter 內部的,函式 inner 對外部是不可見的,因此函式 outter 向外界隱藏了實現細節 inner,被稱為資訊隱藏。

1.3 實現資訊隱藏的例子

實現一個複雜功能的函式時,在函式內部定義大量的輔助函式,這些輔助函式對外不可見。例如,假設要實現一個函式 complex,函式的功能非常複雜,將函式 complex 的功能分解為 3 個子功能,使用三個輔助函式 f1、f2、f3 完成對應的子功能,程式碼如下:

def f1():
    print('Inside f1')

def f2():
    print('Inside f2')

def f3():
    print('Inside f3')

def complex():
    print('Inside complex')
    f1()
    f2()
    f3()
  • 在第 1 行,定義了輔助函式 f1
  • 在第 4 行,定義了輔助函式 f2
  • 在第 7 行,定義了輔助函式 f3
  • 在第 10 行,定義了主函式 complex,它通過呼叫 f1、f2、f3 實現自己的功能

在以上的實現中,函式 f1、f2、f3 是用於實現 complex 的輔助函式,我們希望它們僅僅能夠被 complex 呼叫,而不會被其它函式呼叫。如果可以將函式 f1、f2、f3 定義在函式 complex 的內部,如下所示:

def complex():
    def f1():
        print('Inside f1')
    def f2():
        print('Inside f2')
    def f3():
        print('Inside f3')

    print('Inside complex')
    f1()
    f2()
    f3()
  • 在第 2 行,在函式 complex 內部定義函式 f1
  • 在第 4 行,在函式 complex 內部定義函式 f2
  • 在第 6 行,在函式 complex 內部定義函式 f3
  • 在第 10 行到第 12 行,呼叫 f1、f2、f3 實現函式 complex 的功能

2. 內部函式訪問外部函式的區域性變數

巢狀定義函式時,內部函式可能需要訪問外部函式的變數,例子程式碼如下:

def outter():
    local = 123

    def inner(local):
        print('Inside inner, local = %d', local)

    inner(local)

outter()    
  • 在第 1 行,定義了外部函式 outter
  • 在第 2 行,定義了函式 outter 的區域性變數 local
  • 在第 4 行,定義了內部函式 inner
    • 函式 inner 需要訪問函式 outter 的區域性變數 local
  • 在第 7 行,將函式 outter 的區域性變數 local 作為引數傳遞給函式 inner
    • 在第 5 行,函式 inner 就可以訪問函式 outter 的區域性變數 local

程式執行結果如下:

Inside inner, local = 123

在上面的例子中,將外部函式 outter 的區域性變數 local 作為引數傳遞給內部函式 inner。Python 允許內部函式 inner 不通過引數傳遞直接訪問外部函式 outter 的區域性變數,簡化了引數傳遞,程式碼如下:

def outter():
    local = 123

    def inner():
        print('Inside inner, local = %d', local)

    inner()
  • 在第 1 行,定義了外部函式 outter
  • 在第 2 行,定義了函式 outter 的區域性變數 local
  • 在第 4 行,定義了內部函式 inner
    • 函式 inner 需要訪問函式 outter 的區域性變數 local
  • 在第 5 行,函式 inner 可以直接訪問函式 outter 的區域性變數 local
  • 在第 7 行,不用傳遞引數,直接呼叫函式 inner()

3. 區域性變數的生命週期

通常情況下,函式執行完後,函式內部的區域性變數就不存在了。在巢狀定義函式的情況下,如果內部函式訪問了外部函式的區域性變數,外部函式執行完畢後,內部函式仍然可以訪問外部函式的區域性變數。示例程式碼如下:

def outter():
    local = 123

    def inner():
        print('Inside inner, local = ' % local)

    return inner

closure = outter()
closure()
  • 在第 1 行,定義了外部函式 outter
  • 在第 2 行,定義了函式 outter 的區域性變數 local
  • 在第 4 行,定義了內部函式 inner
    • 函式 inner 需要訪問函式 outter 的區域性變數 local
  • 在第 7 行,將函式 inner 作為值返回
  • 在第 9 行,呼叫函式 outter(),將返回值儲存到變數 closure 中
  • 在第 10 行,呼叫函式 closure()

執行程式,輸出結果如下:

Inside inner, local = 123

注意:在第 10 行,呼叫函式 closure() 時,外部函式 outter 已經執行完,外部函式 outter 將內部函式 inner 返回並儲存到變數 closure。呼叫函式 closure() 相當於呼叫內部函式 inner(),因此,在外部函式 outter 已經執行完的情況下,內部函式 inner 仍然可以訪問外部函式的區域性變數 local。

4. 閉包的概念

閉包的英文是 closure,維基百科中閉包的嚴謹定義如下:

在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包(function closures),是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。—— 維基百科

在本節,以上一節具體的例子說明和理解閉包的概念,上一節的例子程式如下:

def outter():
    local = 123

    def inner():
        print('Inside inner, local = ' % local)

    return inner

closure = outter()
closure()
  • 在第 2 行,區域性變數 local 就是自由變數
  • 在第 5 行,內部函式 inner 引用了局部變數 local (即自由變數)

因此,對照閉包的定義,外部函式定義了局部變數 local,引用了局部變數 local 的內部函式 inner 就是閉包。閉包的獨特之處在於:外部函式 outter 創造了局部變數 local, 即使外部函式 outter 已經執行完,內部函式 inner 仍然可以繼續訪問它引用的區域性變數 local。

5. 閉包的應用

5.1 概述

閉包經常用於 GUI 程式設計的事件響應處理函式。程式語言 Javascript 被用於瀏覽器的使用者介面互動,使用 Javascript 編寫事件響應處理函式時,閉包也是經常提及的知識點。

本小節通過編寫一個簡單的 Python GUI 程式,瞭解為什麼需要使用閉包的語法特性,才方便實現功能需求。

5.2 Tk 簡介

Tkinter 是 Python 的標準 GUI 庫,Python 使用 Tkinter 可以快速的建立 GUI 應用程式。由於 Tkinter 是內建到 python 的安裝包中,只要安裝好 Python 之後就能使用 Tkinter 庫。

由於 Tkinter 簡單易學並且不需要安裝,因此選擇使用 Tk 編寫應用閉包的例子程式。

5.3 例子 1:顯示一個視窗

下面使用 Tk 編寫一個顯示視窗的程式,程式碼如下:

import tkinter

root = tkinter.Tk()
root.mainloop()
  • 在第 1 行,引入 Tk 庫,Tk 庫的名稱是 tkinter
  • 在第 3 行,tkinter.Tk 方法會建立一個視窗 root
  • 在第 4 行,root.mainloop 方法等待使用者的操作

執行程式,顯示輸出如下:

24 Python 的閉包簡介

5.4 例子 2:顯示一個 button

下面使用 Tk 編寫一個顯示 button 的程式,程式碼如下:

import tkinter

root = tkinter.Tk()
button = tkinter.Button(root, text = 'Button')
button.pack()
root.mainloop()
  • 在第 4 行,tkinter.Button 方法建立一個新的 Button,它有兩個引數:第一個引數 root,指定在 root 視窗中建立 Button;第二個引數 text,指定新建立 Button 的標籤
  • 在第 5 行,button.pack 方法將 button 放置在 root 視窗中

執行程式,顯示輸出如下:

24 Python 的閉包簡介

5.5 例子 3:為 button 增加一個事件處理函式

當 button 被點選時,希望程式得到通知,需要為 button 增加一個事件處理函式,程式碼如下:

import tkinter

def on_button_click():
    print('Button is clicked')

root = tkinter.Tk()
button = tkinter.Button(root, text = 'Button', command = on_button_click)
button.pack()
root.mainloop()
  • 在第 3 行,定義了函式 on_button_click,當用戶點選 button 時,程式得到通知,執行 on_btton_click
  • 在第 4 行,函式 on_button_click 在控制檯列印輸出 ‘Button is clicked’
  • 在第 7 行,tkinter.Button 建立一個 Button,設定 3 個引數
    • 引數 root,表示在 root 視窗中建立 button
    • 引數 text,表示 button 的標籤
    • 引數 command,表示當 button 被點選時,對應的事件處理函式
  • 在第 8 行,root.mainloop 等待使用者的操作,當用戶點選 button 時,程式會執行 button 對應的事件處理函式,即執行 on_button_click

執行程式,顯示輸出如下:

24 Python 的閉包簡介


當用戶點選 button 時,執行 on_button_click,在控制檯中列印 ‘Button is clicked’,顯示輸出如下:

24 Python 的閉包簡介

5.6 如何實現計算器

由於篇幅,本節沒有實現一個完整的計算器,在這裡僅僅討論實現計算器程式的關鍵要點。windows 自帶的計算器的介面如下所示:

24 Python 的閉包簡介

計算器向用戶展示各種按鈕,包括:
  • 數字按鍵,0、1、2、3、4、5、6、7、9
  • 運算子按鍵,+、-、*、\、=

使用者在點選某個按鍵時,程式得到通知:按鍵被點選了,但是這樣的資訊還不夠,為了實現運算邏輯,還需要知道具體是哪一個按鍵被點選了。

為了區分是哪一個按鍵被點選了,可以為不同的按鍵設定不同的按鍵處理函式,如下所示:

import tkinter

def on_button0_click():
    print('Button 0 is clicked')

def on_button1_click():
    print('Button 1 is clicked')

def on_button2_click():
    print('Button 2 is clicked')

root = tkinter.Tk()

button0 = tkinter.Button(root, text = 'Button 0', command = on_button0_click)
button0.pack()

button1 = tkinter.Button(root, text = 'Button 1', command = on_button0_click)
button1.pack()

button2 = tkinter.Button(root, text = 'Button 2', command = on_button0_click)
button2.pack()

root.mainloop()

為了節省篇幅,這裡僅僅處理了 3 個按鍵。顯然,這樣的方式是很不合理的,在一個完整的計算器程式中,存在 20 多個按鍵,如果對每個按鍵都編寫一個事件處理函式,就需要編寫 20 多個事件處理函式。在下面的小節中,通過使用閉包解決這個問題。

5.7 例子 4:使用閉包為多個 button 增加事件處理函式

在上面的小節中,面臨的問題是:需要為每個 button 編寫一個事件處理函式。本小節編寫一個事件處理函式響應所有的按鍵點選事件,程式碼如下:

import tkinter

def build_button(root, i):
    def on_button_click():
        print('Button %d is clicked' % i)

    title = 'Button ' + str(i)
    button = tkinter.Button(root, text = title, command = on_button_click)
    button.pack()

root = tkinter.Tk()
for i in range(3):
    build_button(root, i)
root.mainloop()
  • 在第 11 行,tkinter.Tk 建立視窗 root
  • 在第 12 行,使用 for 迴圈呼叫 build_button 建立 3 個 button
  • 在第 14 行,root.mainloop 等待使用者操作
  • 在第 3 行,定義函式 build_button 建立 1 個 button
    • 引數 root,表示在 root 視窗中建立 button
    • 引數 i,表示 button 的序號
  • 在第 4 行,定義事件處理函式 on_button_click
    • build_button 是外部函式
    • on_button_click 是內部函式
    • 在第 5 行,列印外部函式 build_button 的引數 i,因此 on_button_click 是一個閉包函式
  • 在第 7 行,根據 button 的序號 i 設定 button 的標籤
  • 在第 7 行,建立一個 button,設定標籤和事件處理函式

執行程式,顯示輸出如下:

24 Python 的閉包簡介


當用戶點選不同的 button 時,都是執行 on_button_click,但在控制檯中列印的字串是不一樣的,顯示輸出如下:

24 Python 的閉包簡介


在這個例子中,外部函式 build_button 提供了引數 i 用於區分 button,內部函式 on_button_click 可以訪問外部函式的引數。因此,當 button 被點選時,通過引數 i 知道是哪一個 button 被點選了,編寫 1 個事件處理函式就可以處理多個 button 的點選事件,即使用閉包就很自然的解決了實現計算器程式需要面臨的問題。

6. 小結

從概念上來看這一個小節還是比較晦澀的,我在文章的開頭也說過了初學者可以先跳過這一小節,等後面在轉過頭來學習。閉包這個概念非常的重要,面試中有很多面試官喜歡問閉包相關的問題,大家一定要多看幾遍,徹底掌握閉包。