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

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

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

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

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

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ā)生了什么有關,也在某種程度上揭示了閉包的本質(zhì)。

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

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

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

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

用代碼驗證這個順序:

x = "global"  # G 層

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

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

    inner()

outer()

每層可以定義和上層同名的變量,彼此互不干擾。Python 之所以這樣做,是因為名字查找發(fā)生在運行時而非編譯時——解釋器執(zhí)行到哪一行,才去相應的作用域里找變量。

LEGB 規(guī)則可視化:

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

三、global關鍵字:打破 E 層

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

count = count + 1

Python 看到這行代碼時,發(fā)現(xiàn)等號左邊有 count,就認為 count 應該是當前作用域的局部變量。但 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 有個嚴重問題:它讓 count 變成模塊級全局變量。多個 make_counter() 實例會共享同一個 count,完全破壞了隔離性。

四、nonlocal關鍵字:訪問 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ū)別:

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

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

理解 nonlocal 后,再看閉包的完整執(zhí)行流程?;氐阶畛鯃箦e的代碼,但這次不加任何關鍵字:

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

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

    return counter

讀操作(不加 nonlocal)不會報錯——Python 允許讀取外層變量。只有寫操作(count = somethingcount += something)才會觸發(fā) UnboundLocalError,因為等號左邊讓 Python 認為這是一個新的 L 層變量。

當 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())

關鍵字節(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 的實現(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ù)退出后,局部變量通常應該被銷毀——但 doubler 還在使用它,所以 Python 的垃圾回收機制檢測到 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

一個更復雜的例子,驗證多個閉包共享同一個外層變量:

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ù)等場景。

七、閉包的典型應用場景

場景一:函數(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 各有獨立的 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)用都能訪問同一個緩存——避免了普通遞歸中子問題被重復計算的問題。

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

裝飾器本質(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ù)。

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

這是閉包里最隱蔽的錯誤。當閉包在循環(huán)中創(chuàng)建時,所有閉包實例捕獲的是同一個變量,而變量的值以閉包被調(diào)用時的值為準——而不是創(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。

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

def create_multipliers_fixed():
    multipliers = []
    for i in range(5):
        multipliers.append(lambda x, i=i: x * i)  # i=i 把當前值綁定為默認值
    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 是自由變量,在定義時取值為當前的循環(huán)變量;左邊 i 是默認參數(shù),綁定到 lambda 的 L 層作用域。每次循環(huán)迭代時,i 的當前值被"拍"進默認參數(shù),之后循環(huán)繼續(xù),i 變化也不影響已經(jīng)綁定好的默認參數(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 一一對應
>>> 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__ 是這些自由變量對應的 cell 對象序列,兩者順序一致。

十、知識點總結(jié)

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

相關文章

  • 詳解基于python-django框架的支付寶支付案例

    詳解基于python-django框架的支付寶支付案例

    這篇文章主要介紹了基于python-django框架的支付寶支付案例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-09-09
  • 使用llama?Index幫你訓練pdf的示例詳解

    使用llama?Index幫你訓練pdf的示例詳解

    這篇文章主要為大家介紹了使用llama?Index?幫你訓練pdf,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-03-03
  • Python: glob匹配文件的操作

    Python: glob匹配文件的操作

    這篇文章主要介紹了Python: glob匹配文件的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • 關于python處理大型json文件的方法

    關于python處理大型json文件的方法

    這篇文章主要介紹了python處理大型json文件的方法,本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-03-03
  • Python可視化之pyechart庫使用詳解

    Python可視化之pyechart庫使用詳解

    這篇文章主要介紹了Python可視化之pyechart庫使用詳解,Pyecharts 提供了一個簡單而直觀的 API 接口,使得使用者無需了解復雜的 JavaScript 語法,即可通過 Python 代碼實現(xiàn)高度定制化的圖表設計,需要的朋友可以參考下
    2023-12-12
  • python轉(zhuǎn)化excel數(shù)字日期為標準日期操作

    python轉(zhuǎn)化excel數(shù)字日期為標準日期操作

    這篇文章主要介紹了python轉(zhuǎn)化excel數(shù)字日期為標準日期操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-07-07
  • python缺失值的解決方法總結(jié)

    python缺失值的解決方法總結(jié)

    在本篇文章里小編給大家整理的是一篇關于python缺失值的解決方法總結(jié),有需要的朋友們可以跟著學習下。
    2021-06-06
  • PyQt5 實現(xiàn)給窗口設置背景圖片的方法

    PyQt5 實現(xiàn)給窗口設置背景圖片的方法

    今天小編就為大家分享一篇PyQt5 實現(xiàn)給窗口設置背景圖片的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-06-06
  • Python中使用tkFileDialog實現(xiàn)文件選擇、保存和路徑選擇

    Python中使用tkFileDialog實現(xiàn)文件選擇、保存和路徑選擇

    這篇文章主要介紹了Python中使用tkFileDialog實現(xiàn)文件選擇、保存和路徑選擇,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-05-05
  • pytorch神經(jīng)網(wǎng)絡從零開始實現(xiàn)多層感知機

    pytorch神經(jīng)網(wǎng)絡從零開始實現(xiàn)多層感知機

    這篇文章主要為大家介紹了pytorch神經(jīng)網(wǎng)絡從零開始實現(xiàn)多層感知機的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步
    2021-10-10

最新評論

遂溪县| 大洼县| 耒阳市| 通山县| 正阳县| 亚东县| 灌云县| 拜城县| 穆棱市| 睢宁县| 山西省| 大兴区| 文安县| 中牟县| 廉江市| 巴中市| 雅江县| 霞浦县| 广宗县| 城步| 桐梓县| 凤城市| 平陆县| 华亭县| 南通市| 左贡县| 武胜县| 颍上县| 武冈市| 黔西县| 凌海市| 同江市| 开封县| 郧西县| 台前县| 巴南区| 措勤县| 天全县| 渝中区| 武川县| 永定县|