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 方法等待使用者的操作
執行程式,顯示輸出如下:

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 視窗中
執行程式,顯示輸出如下:

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
執行程式,顯示輸出如下:

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

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

- 數字按鍵,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,設定標籤和事件處理函式
執行程式,顯示輸出如下:

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

在這個例子中,外部函式 build_button 提供了引數 i 用於區分 button,內部函式 on_button_click 可以訪問外部函式的引數。因此,當 button 被點選時,通過引數 i 知道是哪一個 button 被點選了,編寫 1 個事件處理函式就可以處理多個 button 的點選事件,即使用閉包就很自然的解決了實現計算器程式需要面臨的問題。
6. 小結
從概念上來看這一個小節還是比較晦澀的,我在文章的開頭也說過了初學者可以先跳過這一小節,等後面在轉過頭來學習。閉包這個概念非常的重要,面試中有很多面試官喜歡問閉包相關的問題,大家一定要多看幾遍,徹底掌握閉包。
