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

Python中閉包變量作用域踩坑實錄

 更新時間:2026年06月03日 14:25:24   作者:站大爺IP  
這篇文章主要為大家詳細介紹了Python項目開發(fā)中閉包變量作用域的踩坑實錄,幫助大家理解閉包如何記住變量,避免常見錯誤,掌握正確的修復(fù)方法

一個讓我加班兩小時的Bug

先給你講個真實故事。

上周三晚上十一點,我還在工位上盯著屏幕。面前是一個跑了好幾天都沒問題的Python腳本,今天突然出了個詭異的bug。

代碼邏輯很簡單:用循環(huán)創(chuàng)建幾個函數(shù),每個函數(shù)記住自己對應(yīng)的數(shù)字。

我當時寫的大概是這樣:

funcs = []
for i in range(3):
    def f():
        print(i)
    funcs.append(f)

for f in funcs:
    f()

我本以為會輸出:

0
1
2

結(jié)果你猜怎么著?

2
2
2

三個2。

我當時第一反應(yīng)是:“這不可能”。Python這么簡單的循環(huán)閉包怎么可能出錯?一定是我哪里寫錯了。

檢查了三遍代碼,沒錯。又檢查了三遍,還是沒錯。

那一刻我感覺自己像是個剛開始學(xué)Python的新手。

后來查了半小時資料,才發(fā)現(xiàn)——原來是我對閉包的理解一直有問題

今天就把這個坑徹底講清楚。

先搞明白:什么是閉包?

先說定義。閉包就是一個函數(shù),它記住了外部作用域的變量。

舉個例子:

def outer():
    msg = "hello"
    def inner():
        print(msg)
    return inner

my_func = outer()
my_func()  # 輸出 hello

這里的 inner 就是一個閉包。它記住了 outer 函數(shù)里的 msg 變量,即使 outer 已經(jīng)執(zhí)行完了。

看起來很簡單對吧?那為什么開頭的例子會出問題?

問題核心:變量什么時候被“記住”?

很多人(包括以前的我)以為:定義函數(shù)的時候,它就會記住當前變量的值

錯。

實際上:函數(shù)被調(diào)用的時候,才會去查找變量的值。

這就是關(guān)鍵。

看回開頭那個例子:

funcs = []
for i in range(3):
    def f():
        print(i)
    funcs.append(f)

循環(huán)創(chuàng)建了三個函數(shù),每個函數(shù)都在說“我要打印變量 i 的值”。

但這個 i 是誰?不是每個函數(shù)的私有拷貝,而是同一個變量 i。

循環(huán)結(jié)束后,i 的值變成了 2。

然后你調(diào)用這些函數(shù):

for f in funcs:
    f()

每個函數(shù)都去查找當前環(huán)境里變量 i 的值——找到了,是 2

于是三個函數(shù)都打印 2。

這就是真相:閉包捕獲的是變量本身,不是變量的值。

用比喻理解這個問題

想象你在一棟寫字樓里。

第1層、第2層、第3層各有一家公司。每家公司都裝了一個顯示屏,顯示“當前樓層號”。

本來每層的顯示屏應(yīng)該顯示不同的數(shù)字:1、2、3。

但現(xiàn)在有個問題——這三塊顯示屏都連著同一個傳感器,這個傳感器告訴你“當前電梯停在哪一層”。

你一開始把電梯停到1樓,傳感器顯示1。但突然有人按了電梯,電梯跑到2樓了。傳感器現(xiàn)在顯示2,三塊顯示屏同時更新成2。電梯再跑到3樓,三塊屏又同時變成3。

最后電梯停在2樓不動了,三塊屏就都顯示2。

你是不是覺得這樣設(shè)計很蠢?每個樓層明明應(yīng)該有自己的數(shù)字。

對,Python這個行為就有點像那個“共用傳感器”的設(shè)計。

常見的錯誤寫法

這種錯誤不只出現(xiàn)在循環(huán)里。來看另一個場景:

def create_multipliers():
    multipliers = []
    for n in [1, 2, 3]:
        multipliers.append(lambda x: x * n)
    return multipliers

for m in create_multipliers():
    print(m(5))

你可能會以為輸出:

5
10
15

實際輸出:

15
15
15

因為 n 最終變成了 3,三個 lambda 用的都是最后那個 n。

解決方案一:默認參數(shù)

最常見的修復(fù)方法:給函數(shù)加一個默認參數(shù)。

funcs = []
for i in range(3):
    def f(i=i):  # 注意這里
        print(i)
    funcs.append(f)

for f in funcs:
    f()  # 輸出 0 1 2

為什么這樣就可以了?

因為 Python 的默認參數(shù)在函數(shù)定義時就會被求值

也就是說,當循環(huán)執(zhí)行到 i=0 時,f(i=i) 里面的第二個 i 被立即計算成數(shù)字 0,然后作為默認值綁定到這個函數(shù)上。

每個函數(shù)有了自己獨立的默認值,不再依賴外部的變量 i。

解決方案二:functools.partial

如果你覺得默認參數(shù)寫法有點“取巧”,可以用更明確的 partial

from functools import partial

funcs = []
for i in range(3):
    def f(x):
        print(x)
    funcs.append(partial(f, i))

for f in funcs:
    f()

partial 就是提前把參數(shù)固定住,生成一個新函數(shù)。

解決方案三:再包一層函數(shù)

也可以再包一層函數(shù)來“凍結(jié)”當前的值:

funcs = []
for i in range(3):
    def outer(i):
        def inner():
            print(i)
        return inner
    funcs.append(outer(i))

for f in funcs:
    f()

outer(i) 被調(diào)用時,參數(shù) i 的值被固定在這個局部作用域里。后面的閉包 inner 記住的是這個局部變量,不是循環(huán)里的那個 i

為什么初學(xué)者特別容易踩這個坑?

因為很多其他語言在類似的場景下行為不同。

比如 JavaScript(ES6 之前)也有類似問題,但如果你用 let 聲明循環(huán)變量,每次迭代會創(chuàng)建一個新的綁定。

Python 的 for 循環(huán)不會為每次迭代創(chuàng)建新的作用域。循環(huán)里的變量就是同一個變量,反復(fù)被賦新值。

而且 Python 的 lambda 和嵌套函數(shù)語法很簡潔,讓人覺得好像“隨手一寫就行”,結(jié)果正好踩坑。

更隱蔽的坑:修改外部變量

閉包默認只能讀取外部變量。如果想修改,需要 nonlocal 關(guān)鍵字。

看這個例子:

def counter():
    count = 0
    def increment():
        count += 1  # 報錯!
        return count
    return increment

c = counter()
c()

運行會報錯:

UnboundLocalError: local variable 'count' referenced before assignment

因為 count += 1 相當于 count = count + 1,Python 在函數(shù)內(nèi)部看到 count = 就會認為 count 是一個局部變量。但局部變量 count 還沒來得及賦值就被引用了。

修復(fù)方法:

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

nonlocal 告訴 Python:“這個 count 不是我的局部變量,去外一層作用域找”。

閉包的內(nèi)存陷阱

還有一個容易被忽略的點:閉包會一直持有外部變量,即使外部函數(shù)已經(jīng)返回了。

看這個例子:

def outer():
    large_data = [0] * 10000000  # 一個大列表
    def inner():
        return len(large_data)
    return inner

func = outer()
# 此時 large_data 應(yīng)該被銷毀嗎?并不會
# 因為 inner 還在引用它

large_data 不會被垃圾回收,只要 func 還存在。

如果你不小心在循環(huán)里創(chuàng)建了一堆閉包,每個都引用了大對象,內(nèi)存可能會暴漲。

實用的調(diào)試技巧

當你懷疑閉包變量出問題時,可以用 __closure__ 屬性檢查:

funcs = []
for i in range(3):
    def f():
        print(i)
    funcs.append(f)

for f in funcs:
    print(f.__closure__)

輸出類似:

(<cell at 0x...: int object at 0x...>,)
(<cell at 0x...: int object at 0x...>,)
(<cell at 0x...: int object at 0x...>,)

你可以進一步查看閉包里的值:

for f in funcs:
    print(f.__closure__[0].cell_contents)

輸出:

2
2
2

果然,三個閉包引用的都是同一個 cell 對象,里面存著 2。

這個工具在排查復(fù)雜閉包問題時非常有用。

再看一個經(jīng)典面試題

很多 Python 面試會考這個:

def create_funcs():
    return [lambda x: x * i for i in range(5)]

for f in create_funcs():
    print(f(2))

輸出是什么?

如果你讀到這里,應(yīng)該能立刻說出答案:

8
8
8
8
8

因為 i 最終是 4,2*4=8。

修復(fù)方法:

def create_funcs():
    return [lambda x, i=i: x * i for i in range(5)]

或者:

def create_funcs():
    return [lambda x, n=i: x * n for i in range(5)]

總結(jié):記住這三句話就夠了

  1. 閉包捕獲的是變量本身,不是變量的值。 變量變了,閉包看到的值也跟著變。
  2. 默認參數(shù)在定義時求值。 想“凍結(jié)”當前值,就用默認參數(shù)。
  3. 修改外部變量要加 nonlocal。 否則 Python 會當成本地變量處理。

這三點說起來簡單,但每一條背后都有實際踩坑的故事。

回到開頭那個讓我加班到深夜的Bug。最后怎么解決的?用了默認參數(shù),三行代碼,一分鐘改完。

但為了搞清楚“為什么”,花了兩小時。

很多時候就是這樣——改代碼很快,真正花時間的是理解“原來我一直想錯了”。

希望這篇文章能幫你省下那兩小時。

下次寫閉包的時候,多問自己一句:“我捕獲的是變量,還是變量的值?”

想清楚這個問題,這個坑就再也坑不到你了。

以上就是Python中閉包變量作用域踩坑實錄的詳細內(nèi)容,更多關(guān)于Python踩坑實錄的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Python中免驗證跳轉(zhuǎn)到內(nèi)容頁的實例代碼

    Python中免驗證跳轉(zhuǎn)到內(nèi)容頁的實例代碼

    在本篇文章里小編給大家整理的是一篇關(guān)于Python中免驗證跳轉(zhuǎn)到內(nèi)容頁的實例代碼,有興趣的朋友們可以學(xué)習(xí)分享下。
    2020-10-10
  • 對比分析BN和dropout在預(yù)測和訓(xùn)練時區(qū)別

    對比分析BN和dropout在預(yù)測和訓(xùn)練時區(qū)別

    這篇文章主要為大家介紹了對比分析BN和dropout在預(yù)測和訓(xùn)練時區(qū)別,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-05-05
  • Python列表list常用內(nèi)建函數(shù)實例小結(jié)

    Python列表list常用內(nèi)建函數(shù)實例小結(jié)

    這篇文章主要介紹了Python列表list常用內(nèi)建函數(shù),結(jié)合實例形式總結(jié)分析了Python列表list常見內(nèi)建函數(shù)的功能、使用方法及相關(guān)操作注意事項,需要的朋友可以參考下
    2019-10-10
  • Python生命游戲?qū)崿F(xiàn)原理及過程解析(附源代碼)

    Python生命游戲?qū)崿F(xiàn)原理及過程解析(附源代碼)

    這篇文章主要介紹了Python生命游戲?qū)崿F(xiàn)原理及過程解析(附源代碼),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-08-08
  • Python操作SQLite數(shù)據(jù)庫過程解析

    Python操作SQLite數(shù)據(jù)庫過程解析

    這篇文章主要介紹了Python操作SQLite數(shù)據(jù)庫過程解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2019-09-09
  • Python3 導(dǎo)入上級目錄中的模塊實例

    Python3 導(dǎo)入上級目錄中的模塊實例

    今天小編就為大家分享一篇Python3 導(dǎo)入上級目錄中的模塊實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-02-02
  • Python辦公自動化之將任意文件轉(zhuǎn)為PDF格式

    Python辦公自動化之將任意文件轉(zhuǎn)為PDF格式

    這種把某個文件轉(zhuǎn)為pdf枯燥無聊的工作,既沒有什么技術(shù)含量又累. 今天辰哥就教大家將任意文件批量轉(zhuǎn)為PDF,這里以日常辦公的word、excel、ppt為例,這三種格式的文件轉(zhuǎn)為PDF.需要的朋友可以參考下
    2021-06-06
  • pycharm中創(chuàng)建sql文件及模板的過程

    pycharm中創(chuàng)建sql文件及模板的過程

    很多小伙伴剛開始使用pycharm時發(fā)現(xiàn)以前的老員工在使用pycharm創(chuàng)建sql文件時會自帶文件頭模板,例如時間、作者、版本、郵件等信息,這是怎么做到的呢,一起來看一下吧
    2022-07-07
  • Tensorflow的梯度異步更新示例

    Tensorflow的梯度異步更新示例

    今天小編就為大家分享一篇Tensorflow的梯度異步更新示例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-01-01
  • python?爬取豆瓣電影短評并利用wordcloud生成詞云圖

    python?爬取豆瓣電影短評并利用wordcloud生成詞云圖

    這篇文章主要介紹了python?爬取豆瓣電影短評并利用wordcloud生成詞云圖,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下
    2022-06-06

最新評論

烟台市| 临沂市| 鹿邑县| 陇南市| 丰镇市| 新乡市| 天峨县| 翁牛特旗| 平潭县| 汉中市| 达孜县| 当阳市| 尚志市| 建瓯市| 陕西省| 阿荣旗| 神池县| 右玉县| 甘肃省| 镇远县| 夏河县| 西林县| 西乌珠穆沁旗| 那坡县| 大同市| 正蓝旗| 巴青县| 钟山县| 敦煌市| 盐城市| 游戏| 金平| 仙游县| 左贡县| 靖宇县| 随州市| 乐平市| 靖州| 界首市| 筠连县| 湟中县|