最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

淺析Python閉包如何捕獲自由變量

 更新時間:2026年05月06日 09:04:29   作者:SilentSamsara  
本文主要講述了Python中作用域和閉包的相關(guān)知識,包括LEGB作用域規(guī)則、global和nonlocal關(guān)鍵字的區(qū)別與應(yīng)用、閉包的完整執(zhí)行流程、閉包變量的生命周期以及閉包的常見應(yīng)用場景和錯誤處理

一、從一個計數(shù)器開始

學(xué)作用域的時候,通常會遇到這樣的代碼:

def make_counter():
    count = 0
    def counter():
        count += 1   # 這里會報錯
        return count
    return counter

c = make_counter()
print(c())  # UnboundLocalError: local variable 'count' referenced before assignment

這不是 bug——這是 Python 作用域規(guī)則在說話。報錯的原因和 count += 1 這行代碼里發(fā)生了什么有關(guān),也在某種程度上揭示了閉包的本質(zhì)。

要徹底理解這段代碼,得先搞清楚 Python 的作用域規(guī)則,以及閉包是怎么工作的。

二、LEGB 規(guī)則:名字查找的順序

Python 查找變量名時,按 LEGB 的順序逐層搜索:

  • L(Local):當(dāng)前函數(shù)內(nèi)部定義的變量
  • E(Enclosing):外層嵌套函數(shù)中的變量
  • G(Global):模塊級全局變量
  • B(Built-in):Python 內(nèi)置名字,比如 len、print

用代碼驗(yàn)證這個順序:

x = "global"  # G 層

def outer():
    x = "enclosing"  # E 層

    def inner():
        x = "local"  # L 層
        print(x)  # -> local(找到了就停)

    inner()

outer()

每層可以定義和上層同名的變量,彼此互不干擾。Python 之所以這樣做,是因?yàn)槊植檎野l(fā)生在運(yùn)行時而非編譯時——解釋器執(zhí)行到哪一行,才去相應(yīng)的作用域里找變量。

LEGB 規(guī)則可視化:

查找時從內(nèi)向外逐層搜索,找到即停。

三、global關(guān)鍵字:打破 E 層

回到開頭的計數(shù)器報錯。count += 1 等價于:

count = count + 1

Python 看到這行代碼時,發(fā)現(xiàn)等號左邊有 count,就認(rèn)為 count 應(yīng)該是當(dāng)前作用域的局部變量。但 countouter() 的作用域里(E 層),不在 counter() 的作用域里(L 層)——Python 拒絕在 E 層創(chuàng)建同名 L 層變量,所以報了 UnboundLocalError。

解決方式一:把 count 提升到全局作用域:

count = 0

def make_counter():
    global count  # 聲明接下來訪問全局的 count
    count += 1
    return count

print(make_counter())  # 1
print(make_counter())  # 2

global 有個嚴(yán)重問題:它讓 count 變成模塊級全局變量。多個 make_counter() 實(shí)例會共享同一個 count,完全破壞了隔離性。

四、nonlocal關(guān)鍵字:訪問 E 層變量

nonlocalglobal 的近親,但作用域不同。它允許在 L 層函數(shù)中修改 E 層(嵌套外層)的變量:

def make_counter():
    count = 0

    def counter():
        nonlocal count  # 聲明:接下來對 count 的賦值操作,作用于外層的 count
        count += 1
        return count

    return counter

c = make_counter()
print(c())  # 1
print(c())  # 2
print(c())  # 3

nonlocal 不會在 L 層創(chuàng)建新變量,也不涉及 G 層——它直接作用于最近一層外層函數(shù)的變量。

global vs nonlocal 的區(qū)別:

關(guān)鍵字作用層行為
global模塊級 G 層讀寫全局變量,多個函數(shù)共享
nonlocal嵌套外層 E 層讀寫外層函數(shù)變量,多個閉包獨(dú)立

五、閉包的完整執(zhí)行流程

理解 nonlocal 后,再看閉包的完整執(zhí)行流程。回到最初報錯的代碼,但這次不加任何關(guān)鍵字:

def make_counter():
    count = 0  # <- E 層變量

    def counter():
        print("count 當(dāng)前值:", count)  # 讀 E 層變量 - 沒問題
        return count  # 讀 E 層變量 - 沒問題

    return counter

讀操作(不加 nonlocal)不會報錯——Python 允許讀取外層變量。只有寫操作(count = somethingcount += something)才會觸發(fā) UnboundLocalError,因?yàn)榈忍栕筮呑?Python 認(rèn)為這是一個新的 L 層變量。

當(dāng) Python 看到 nonlocal count 時,在編譯期(生成字節(jié)碼時)就已經(jīng)把這件事記下來了:

import dis

def make_counter():
    count = 0

    def counter():
        nonlocal count
        count += 1
        return count

    return counter

# counter 函數(shù)的字節(jié)碼
dis.dis(counter := make_counter())

關(guān)鍵字節(jié)碼:

  5           2 LOAD_GLOBAL          0 (count)
              4 LOAD_CONST           1 (1)
              6 BINARY_OP            0 (+)
              8 STORE_FAST           0 (count)
              10 LOAD_FAST           0 (count)
             12 RETURN_VALUE

LOAD_GLOBAL 0 (count)nonlocal 的實(shí)現(xiàn)方式——如果不用 nonlocal,這行會變成 LOAD_FAST(讀 L 層變量),然后 STORE_FAST 會觸發(fā) UnboundLocalError。

六、閉包變量的生命周期

閉包有一個經(jīng)常被忽視的特性:閉包變量的生命周期和閉包本身一樣長。

def make_multiplier(factor):
    # factor 綁定在 make_multiplier 的局部作用域里
    def multiply(value):
        return value * factor
    return multiply

doubler = make_multiplier(2)

# make_multiplier() 已經(jīng)執(zhí)行完畢退出了
# 但 doubler 仍然持有 factor=2
print(doubler(5))   # 10
print(doubler(100)) # 200

factor 原本在 make_multiplier() 的局部作用域里。函數(shù)退出后,局部變量通常應(yīng)該被銷毀——但 doubler 還在使用它,所以 Python 的垃圾回收機(jī)制檢測到 factor 仍有外部引用,就把它保留下來,通過 cell 對象包裝后存入 doubler.__closure__。

這就是為什么 doubler.__closure__[0].cell_contents 能讀取到 2——cell 對象就是閉包變量在內(nèi)存中的載體。

>>> doubler.__closure__
(<cell at 0x...: int object at 0x...>,)
>>> doubler.__closure__[0].cell_contents
2

一個更復(fù)雜的例子,驗(yàn)證多個閉包共享同一個外層變量:

def processor(initial=0):
    total = initial

    def add(x):
        nonlocal total
        total += x
        return total

    def subtract(x):
        nonlocal total
        total -= x
        return total

    return add, subtract

add, subtract = processor(100)

print(add(30))     # 130,total = 100 + 30
print(subtract(20)) # 110,total = 130 - 20
print(add(10))     # 120,total = 110 + 10

addsubtract 指向同一個 cell 對象——修改 total 對兩個函數(shù)都生效。這是閉包的"共享狀態(tài)"特性,常用于事件處理器、回調(diào)函數(shù)等場景。

七、閉包的典型應(yīng)用場景

場景一:函數(shù)工廠(最常見用法)

根據(jù)不同參數(shù)生成專用函數(shù):

def power_factory(exp):
    def power(base):
        return base ** exp
    return power

square = power_factory(2)
cube = power_factory(3)

print(square(5))  # 25
print(cube(5))    # 125

exp 被捕獲在閉包里,squarecube 各有獨(dú)立的 exp 值,互不干擾。相比寫死參數(shù),函數(shù)工廠更靈活,避免了為每種指數(shù)寫專門的函數(shù)。

場景二:帶記憶的遞歸函數(shù)

def memoized_fibonacci():
    cache = {}  # E 層變量

    def fib(n):
        if n in cache:
            return cache[n]
        if n <= 1:
            result = n
        else:
            result = fib(n-1) + fib(n-2)
        cache[n] = result
        return result

    return fib

fib = memoized_fibonacci()
print(fib(100))  # 354224848179261915075
print(fib(200))  # 280571172992510140037611908417314019

cache 字典在閉包里持久化,每次遞歸調(diào)用都能訪問同一個緩存——避免了普通遞歸中子問題被重復(fù)計算的問題。

場景三:裝飾器(閉包的直接應(yīng)用)

裝飾器本質(zhì)上就是閉包:

import functools
import time

def timing_decorator(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        # fn 和 elapsed_time 都是閉包變量
        start = time.perf_counter()
        result = fn(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{fn.__name__} 耗時 {elapsed:.4f}s")
        return result
    return wrapper

wrapper 捕獲了 fn(被裝飾的函數(shù))和 elapsed_time(計時變量)兩個自由變量。timing_decorator 返回的 wrapper 閉包里裝著被裝飾函數(shù)的引用,調(diào)用時真正執(zhí)行的是 wrapper,而非原函數(shù)。

八、閉包的常見錯誤:遲綁定

這是閉包里最隱蔽的錯誤。當(dāng)閉包在循環(huán)中創(chuàng)建時,所有閉包實(shí)例捕獲的是同一個變量,而變量的值以閉包被調(diào)用時的值為準(zhǔn)——而不是創(chuàng)建時的值:

def create_multipliers():
    multipliers = []
    for i in range(5):
        multipliers.append(lambda x: x * i)  # i 是自由變量
    return multipliers

fns = create_multipliers()

# 全部返回 4*4=16,而不是 0*4, 1*4, 2*4, 3*4, 4*4
print([fn(4) for fn in fns])  # [16, 16, 16, 16, 16]

循環(huán)結(jié)束時 i = 4,所有閉包引用的是同一個 i,所以調(diào)用時都得到 4 * 4 = 16。

解決方式:用默認(rèn)參數(shù)在閉包創(chuàng)建時立即捕獲當(dāng)前值:

def create_multipliers_fixed():
    multipliers = []
    for i in range(5):
        multipliers.append(lambda x, i=i: x * i)  # i=i 把當(dāng)前值綁定為默認(rèn)值
    return multipliers

fns = create_multipliers_fixed()
print([fn(4) for fn in fns])  # [0, 4, 8, 12, 16]

lambda x, i=i: ... 里,i=i 的右邊 i 是自由變量,在定義時取值為當(dāng)前的循環(huán)變量;左邊 i 是默認(rèn)參數(shù),綁定到 lambda 的 L 層作用域。每次循環(huán)迭代時,i 的當(dāng)前值被"拍"進(jìn)默認(rèn)參數(shù),之后循環(huán)繼續(xù),i 變化也不影響已經(jīng)綁定好的默認(rèn)參數(shù)。

functools.partial 也能解決:

import functools

def create_multipliers_partial():
    multipliers = []
    for i in range(5):
        multipliers.append(functools.partial(lambda x, i: x * i, i=i))
    return multipliers

九、__closure__與自由變量的深度解析

可以用 __code__.co_freevars 直接看到函數(shù)捕獲了哪些自由變量:

def outer(x):
    def inner(y):
        # z 從更外層捕獲
        def deeper(z):
            return x + y + z
        return deeper
    return inner

# 查看各層函數(shù)的自由變量
outer_fn = outer(10)
inner_fn = outer_fn(20)
deeper_fn = inner_fn(30)

>>> outer_fn.__code__.co_freevars
('x',)
>>> inner_fn.__code__.co_freevars
('x', 'y')
>>> deeper_fn.__code__.co_freevars
('x', 'y', 'z')

# __closure__ 的順序和 co_freevars 一一對應(yīng)
>>> deeper_fn.__closure__
(<cell at ...: int object at ...>, <cell at ...: int object at ...>, <cell at ...: int object at ...>)
>>> deeper_fn.__closure__[0].cell_contents, \
     deeper_fn.__closure__[1].cell_contents, \
     deeper_fn.__closure__[2].cell_contents
(10, 20, 30)

co_freevars 是字節(jié)碼層面的元信息,告訴解釋器哪些名字是自由變量;__closure__ 是這些自由變量對應(yīng)的 cell 對象序列,兩者順序一致。

十、知識點(diǎn)總結(jié)

到此這篇關(guān)于淺析Python閉包如何捕獲自由變量的文章就介紹到這了,更多相關(guān)Python閉包捕獲自由變量內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

横峰县| 稻城县| 平潭县| 舒兰市| 福鼎市| 瓮安县| 池州市| 简阳市| 浑源县| 都兰县| 太原市| 东台市| 屏南县| 探索| 宁津县| 大洼县| 夏邑县| 祁东县| 博白县| 正阳县| 伊川县| 望奎县| 齐河县| 宜章县| 扎赉特旗| 三穗县| 邢台市| 黄大仙区| 沁水县| 璧山县| 明光市| 盘山县| 临澧县| 阳新县| 深水埗区| 绥德县| 阳朔县| 北票市| 十堰市| 水城县| 平凉市|