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

Python中閉包變量作用域踩坑實(shí)錄

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

一個(gè)讓我加班兩小時(shí)的Bug

先給你講個(gè)真實(shí)故事。

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

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

我當(dāng)時(shí)寫的大概是這樣:

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

for f in funcs:
    f()

我本以為會(huì)輸出:

0
1
2

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

2
2
2

三個(gè)2。

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

檢查了三遍代碼,沒錯(cuò)。又檢查了三遍,還是沒錯(cuò)。

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

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

今天就把這個(gè)坑徹底講清楚。

先搞明白:什么是閉包?

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

舉個(gè)例子:

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

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

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

看起來很簡(jiǎn)單對(duì)吧?那為什么開頭的例子會(huì)出問題?

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

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

錯(cuò)。

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

這就是關(guān)鍵。

看回開頭那個(gè)例子:

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

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

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

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

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

for f in funcs:
    f()

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

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

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

用比喻理解這個(gè)問題

想象你在一棟寫字樓里。

第1層、第2層、第3層各有一家公司。每家公司都裝了一個(gè)顯示屏,顯示“當(dāng)前樓層號(hào)”。

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

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

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

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

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

對(duì),Python這個(gè)行為就有點(diǎn)像那個(gè)“共用傳感器”的設(shè)計(jì)。

常見的錯(cuò)誤寫法

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

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

你可能會(huì)以為輸出:

5
10
15

實(shí)際輸出:

15
15
15

因?yàn)?n 最終變成了 3,三個(gè) lambda 用的都是最后那個(gè) n。

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

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

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

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

為什么這樣就可以了?

因?yàn)?Python 的默認(rèn)參數(shù)在函數(shù)定義時(shí)就會(huì)被求值

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

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

解決方案二:functools.partial

如果你覺得默認(rèn)參數(shù)寫法有點(diǎn)“取巧”,可以用更明確的 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ù)固定住,生成一個(gè)新函數(shù)。

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

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

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

for f in funcs:
    f()

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

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

因?yàn)楹芏嗥渌Z言在類似的場(chǎng)景下行為不同。

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

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

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

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

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

看這個(gè)例子:

def counter():
    count = 0
    def increment():
        count += 1  # 報(bào)錯(cuò)!
        return count
    return increment

c = counter()
c()

運(yùn)行會(huì)報(bào)錯(cuò):

UnboundLocalError: local variable 'count' referenced before assignment

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

修復(fù)方法:

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

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

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

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

看這個(gè)例子:

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

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

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

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

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

當(dāng)你懷疑閉包變量出問題時(shí),可以用 __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...>,)

你可以進(jìn)一步查看閉包里的值:

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

輸出:

2
2
2

果然,三個(gè)閉包引用的都是同一個(gè) cell 對(duì)象,里面存著 2。

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

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

很多 Python 面試會(huì)考這個(gè):

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

因?yàn)?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. 默認(rèn)參數(shù)在定義時(shí)求值。 想“凍結(jié)”當(dāng)前值,就用默認(rèn)參數(shù)。
  3. 修改外部變量要加 nonlocal。 否則 Python 會(huì)當(dāng)成本地變量處理。

這三點(diǎn)說起來簡(jiǎn)單,但每一條背后都有實(shí)際踩坑的故事。

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

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

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

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

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

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

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

相關(guān)文章

最新評(píng)論

马龙县| 太和县| 华池县| 基隆市| 长丰县| 贺州市| 九龙县| 长丰县| 南投市| 沅陵县| 金华市| 平定县| 虎林市| 桃江县| 资中县| 依兰县| 宝鸡市| 江城| 巴塘县| 贵南县| 漳州市| 江川县| 南溪县| 彰化市| 曲麻莱县| 方城县| 庆云县| 登封市| 岱山县| 霍林郭勒市| 启东市| 内丘县| 宣威市| 枣阳市| 卢龙县| 绥德县| 新余市| 黑龙江省| 皮山县| 磐安县| 西林县|