鍍金池/ 教程/ Python/ 閉包
基礎(chǔ)
itertools
HTTP 服務(wù)
hashlib
閉包
文件和目錄
單元測試
使用 @property
標準模塊
陌生的 metaclass
Base64
進程、線程和協(xié)程
讀寫二進制文件
匿名函數(shù)
輸入和輸出
Click
元組
字符編碼
partial 函數(shù)
參考資料
collections
協(xié)程
類和實例
Python 之旅
定制類和魔法方法
常用數(shù)據(jù)類型
繼承和多態(tài)
ThreadLocal
HTTP 協(xié)議簡介
Requests 庫的使用
讀寫文本文件
列表
os 模塊
迭代器 (Iterator)
正則表達式
集合
上下文管理器
異常處理
你不知道的 super
定義函數(shù)
datetime
資源推薦
字典
slots 魔法
hmac
第三方模塊
進程
類方法和靜態(tài)方法
函數(shù)參數(shù)
高階函數(shù)
函數(shù)
re 模塊
高級特性
線程
argparse
生成器
結(jié)束語
字符串
map/reduce/filter
函數(shù)式編程
Celery
裝飾器

閉包

在 Python 中,函數(shù)也是一個對象。因此,我們在定義函數(shù)時,可以再嵌套定義一個函數(shù),并將該嵌套函數(shù)返回,比如:

from math import pow

def make_pow(n):
    def inner_func(x):     # 嵌套定義了 inner_func
        return pow(x, n)   # 注意這里引用了外部函數(shù)的 n
    return inner_func      # 返回 inner_func

上面的代碼中,函數(shù) make_pow 里面又定義了一個內(nèi)部函數(shù) inner_func,然后將該函數(shù)返回。因此,我們可以使用 make_pow 來生成另一個函數(shù):

>>> pow2 = make_pow(2)  # pow2 是一個函數(shù),參數(shù) 2 是一個自由變量
>>> pow2
<function inner_func at 0x10271faa0>
>>> pow2(6)
36.0

我們還注意到,內(nèi)部函數(shù) inner_func 引用了外部函數(shù) make_pow 的自由變量 n,這也就意味著,當(dāng)函數(shù) make_pow 的生命周期結(jié)束之后,n 這個變量依然會保存在 inner_func 中,它被 inner_func 所引用。

>>> del make_pow         # 刪除 make_pow
>>> pow3 = make_pow(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'make_pow' is not defined
>>> pow2(9)     # pow2 仍可正常調(diào)用,自由變量 2 仍保存在 pow2 中
81.0

像上面這種情況,一個函數(shù)返回了一個內(nèi)部函數(shù),該內(nèi)部函數(shù)引用了外部函數(shù)的相關(guān)參數(shù)和變量,我們把該返回的內(nèi)部函數(shù)稱為閉包(Closure)

在上面的例子中,inner_func 就是一個閉包,它引用了自由變量 n。

閉包的作用

  • 閉包的最大特點就是引用了自由變量,即使生成閉包的環(huán)境已經(jīng)釋放,閉包仍然存在。
  • 閉包在運行時可以有多個實例,即使傳入的參數(shù)相同。
>>> pow_a = make_pow(2)
>>> pow_b = make_pow(2)
>>> pow_a == pow_b
False
  • 利用閉包,我們還可以模擬類的實例。

這里構(gòu)造一個類,用于求一個點到另一個點的距離:

from math import sqrt

class Point(object):
    def __init__(self, x, y):
        self.x, self.y = x, y

    def get_distance(self, u, v):
        distance = sqrt((self.x - u) ** 2 + (self.y - v) ** 2)
        return distance

>>> pt = Point(7, 2)        # 創(chuàng)建一個點
>>> pt.get_distance(10, 6)  # 求到另一個點的距離
5.0

用閉包來實現(xiàn):

def point(x, y):
    def get_distance(u, v):
        return sqrt((x - u) ** 2 + (y - v) ** 2)

    return get_distance

>>> pt = point(7, 2)
>>> pt(10, 6)
5.0

可以看到,結(jié)果是一樣的,但使用閉包實現(xiàn)比使用類更加簡潔。

常見誤區(qū)

閉包的概念很簡單,但實現(xiàn)起來卻容易出現(xiàn)一些誤區(qū),比如下面的例子:

def count():
    funcs = []
    for i in [1, 2, 3]:
        def f():
            return i
        funcs.append(f)
    return funcs

在該例子中,我們在每次 for 循環(huán)中創(chuàng)建了一個函數(shù),并將它存到 funcs 中?,F(xiàn)在,調(diào)用上面的函數(shù),你可能認為返回結(jié)果是 1, 2, 3,事實上卻不是:

>>> f1, f2, f3 = count()
>>> f1()
3
>>> f2()
3
>>> f3()
3

為什么呢?原因在于上面的函數(shù) f 引用了變量 i,但函數(shù) f 并非立刻執(zhí)行,當(dāng) for 循環(huán)結(jié)束時,此時變量 i 的值是3,funcs 里面的函數(shù)引用的變量都是 3,最終結(jié)果也就全為 3。

因此,我們應(yīng)盡量避免在閉包中引用循環(huán)變量,或者后續(xù)會發(fā)生變化的變量

那上面這種情況應(yīng)該怎么解決呢?我們可以再創(chuàng)建一個函數(shù),并將循環(huán)變量的值傳給該函數(shù),如下:

def count():
    funcs = []
    for i in [1, 2, 3]:
        def g(param):
            f = lambda : param    # 這里創(chuàng)建了一個匿名函數(shù)
            return f
        funcs.append(g(i))        # 將循環(huán)變量的值傳給 g
    return funcs

>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
2
>>> f3()
3

小結(jié)

  • 閉包是攜帶自由變量的函數(shù),即使創(chuàng)建閉包的外部函數(shù)的生命周期結(jié)束了,閉包所引用的自由變量仍會存在。
  • 閉包在運行可以有多個實例。
  • 盡量不要在閉包中引用循環(huán)變量,或者后續(xù)會發(fā)生變化的變量。

參考資料