亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

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 = %d' % 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 = %d' % 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 被點擊時,對應的事件處理函數
  • 在第 9 行,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 的標簽
  • 在第 8 行,創建一個 button,設置標簽和事件處理函數

運行程序,顯示輸出如下:

圖片描述
當用戶點擊不同的 button 時,都是執行 on_button_click,但在控制臺中打印的字符串是不一樣的,顯示輸出如下:
圖片描述
在這個例子中,外部函數 build_button 提供了參數 i 用于區分 button,內部函數 on_button_click 可以訪問外部函數的參數。因此,當 button 被點擊時,通過參數 i 知道是哪一個 button 被點擊了,編寫 1 個事件處理函數就可以處理多個 button 的點擊事件,即使用閉包就很自然的解決了實現計算器程序需要面臨的問題。

6. 小結

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