Python中閉包和自由變量的使用與注意事項(xiàng)
1.定義
在函數(shù)內(nèi)部再定義一個(gè)函數(shù),并且這個(gè)函數(shù)用到了外部函數(shù)的變量(LEGB),最后返回新建函數(shù)的函數(shù)名索引,那么將這樣的能夠訪問其定義時(shí)所在的作用域的函數(shù)以及用到的一些變量稱之為閉包。被引用的非全局變量也稱為自由變量 。這個(gè)自由變量保存在外部函數(shù)的只讀屬性 __closure__ 中,會(huì)與內(nèi)層函數(shù)產(chǎn)生一個(gè)綁定關(guān)系,也就是自由變量將不會(huì)在內(nèi)存中輕易消失。如下例所示:
# 計(jì)算函數(shù)被調(diào)用的次數(shù)
def counter(FIRST=0):
-----------------__closure__---------------
|cnt = [FIRST] | # 之所以選列表是因?yàn)樽饔糜騿栴},詳見后文
| |
|def add_one(): |
| cnt[0] += 1 |
| return cnt[0] |
------------------------------------------
return add_one
# 每當(dāng)外部函數(shù)被調(diào)用時(shí),都將重新定義內(nèi)部的函數(shù),而變量 cnt 的值也可能不同
num5 = counter(5)
num10 = counter(10)
print(num5()) # 6
print(num5()) # 7
print(num10()) # 11
print(num10()) # 12
# 如果這個(gè)函數(shù)僅僅是嵌套函數(shù),那么它的 __closure__ 應(yīng)該是 None
print(num5.__closure__) # (<cell at 0x0163FE30: list object at 0x01514A80>,)
print(num5.__closure__[0].cell_contents) # 7
print(num10.__closure__[0].cell_contents) # 12
# 或者通過 __code__.co_freevars 查看函數(shù)中是否有自由變量,如果有自由變量,即為閉包
print(num10.__code__.co_freevars) # ('cnt',)
2.nonlocal 關(guān)鍵字
上面代碼中的 cnt 變量是一個(gè)列表,可變對象,但如果是不可變對象,如:numer、tuple 等呢?
def counter(FIRST=0):
cnt = FIRST # number
def add_one():
cnt += 1
return cnt
return add_one
num5 = counter(5)
print(num5.__closure__)
print(num5.__code__.co_freevars)
print(num5())
----------------------------------------------------------------------------
def counter(FIRST=0):
cnt = (FIRST,) # tuple
def add_one():
cnt[0] += 1
return cnt[0]
return add_one
num5 = counter(5)
print(num5.__closure__)
print(num5.__code__.co_freevars)
print(num5())
以上實(shí)例輸出結(jié)果:
None
()
Traceback (most recent call last):
File "test.py", line, in <module>
print(num5())
File "test.py", line, in add_one
cnt += 1
UnboundLocalError: local variable 'cnt' referenced before assignment
----------------------------------------------------------------------------
(<cell at 0x0180FE10: tuple object at 0x0173A750>,)
('cnt',)
Traceback (most recent call last):
File "test.py", line, in <module>
print(num5())
File "test.py", line, in add_one
cnt[0] += 1
TypeError: 'tuple' object does not support item assignment
可以看出,此時(shí) cnt 不再是自由變量,而是變成了局部變量,且提示 UnboundLocalError 未綁定局部錯(cuò)誤。為什么不是自由變量了呢?為什么列表就沒問題呢?
這是因?yàn)?nbsp;Python 中并沒有要求先聲明一個(gè)變量才能使用它,Python 解釋器認(rèn)為:在函數(shù)體內(nèi),只要對一個(gè)變量進(jìn)行賦值操作,那么這個(gè)變量就是局部變量。
Python的模塊代碼執(zhí)行之前,并不會(huì)經(jīng)過預(yù)編譯,模塊內(nèi)的函數(shù)體代碼在運(yùn)行前會(huì)經(jīng)過預(yù)編譯,因此不管變量名的綁定發(fā)生在作用域的那個(gè)位置,都能被編譯器知道。
而 cnt += 1 相當(dāng)于 cnt = cnt + 1,對 cnt 進(jìn)行了賦值操作,所以 Python 解釋器認(rèn)為 cnt 是函數(shù)內(nèi)的局部變量,但是執(zhí)行的時(shí)候,先執(zhí)行 cnt+1 時(shí)發(fā)現(xiàn):
因?yàn)橄惹耙呀?jīng)認(rèn)定 cnt 為局部變量了,現(xiàn)在在局部作用域內(nèi)找不到 cnt 的值,也不會(huì)再到外部作用域找了,就會(huì)報(bào)錯(cuò)。所以說現(xiàn)在 cnt 已經(jīng)不是自由變量了。
那么 tuple 類型的 cnt 呢?首先 cnt[0] = cnt[0] + 1,雖然有賦值,但是其左邊也是 cnt[0],cnt 是從外邊作用域索引了的。
所以,你看它顯示的結(jié)果:此時(shí),cnt 確實(shí)也是自由變量的,但是它是不可變對象啊,所以報(bào)了 TypeError 錯(cuò)誤。這下列表為什么行,你應(yīng)該知道了。
或者你使用 nonolocal 關(guān)鍵字,這個(gè)關(guān)鍵字的用法與 global 很像,讓你能夠給外部作用域(非全局作用域)內(nèi)的變量賦值。它可以使得一個(gè)被賦值的局部變量變?yōu)樽杂勺兞?,并?nbsp;nonlocal聲明的變量發(fā)生變化時(shí),__closure__中存儲(chǔ)的值也會(huì)發(fā)生變化:
def counter(FIRST=0):
cnt = FIRST # number
def add_one():
nonlocal cnt
cnt += 1
return cnt
return add_one
num5 = counter(5)
print(num5.__closure__)
print(num5.__code__.co_freevars)
print(num5())
(<cell at 0x01BFFE30: int object at 0x53E064D0>,)
('cnt',)
6
nonlocal 和 global
def scope_test():
spam = "test spam"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
def do_global():
global spam
spam = "global spam"
do_nonlocal()
print("After nonlocal assignment:", spam) # nonlocal spam
do_global()
print("After global assignment:", spam) # nonlocal spam
scope_test()
print("In global scope:", spam) # global spam
After nonlocal assignment: nonlocal spam After global assignment: nonlocal spam In global scope: global spam
3.注意事項(xiàng)
lambda 自由參數(shù)之坑,特別是和列表解析或for循環(huán)結(jié)合使用時(shí)。lambda para_list : expression == > def (para_list): return expression
#---CASE1 fs = [lambda j:i*j for i in range(3)] print([f(2) for f in fs]) #---CASE2 fs = map(lambda i:(lambda j: i*j), range(3)) print([f(2) for f in fs]) #---CASE3 fs = [(lambda i:lambda j:i*j)(i) for i in range(3)] print([f(2) for f in fs])
[4, 4, 4] [0, 2, 4] [0, 2, 4]
首先,CASE1 和 CASE3 顯然都是每循環(huán)一次,就添加一個(gè) lambda 函數(shù)到列表中,不同的是,CASE1 添加的 lambda 函數(shù)中的 i 每次并沒有接收 for 循環(huán)中 i 的值,它只是定義的時(shí)候指了下 i,所以說,CASE1 中的幾個(gè) lambda 函數(shù)的 i,是最后調(diào)用的時(shí)候,也就是 f(2) 時(shí)才到外層作用域找它的值的,此時(shí)找到的 i 的值就是里面 for 循環(huán)結(jié)束時(shí)的 i 的值。CASE3 則是一開始定義、添加的時(shí)候就給 i 賦好了初值。CASE2 則是因?yàn)?map 每次迭代的時(shí)候都會(huì)將一個(gè)可迭代對象的元素傳給了 i,所以 CASE2 里面的每個(gè) lambda 函數(shù)的 i 也是各有各的值的。
像這種 lambda 的自由參數(shù)的問題的話,如果你不是故意這么做的話,還是轉(zhuǎn)為默認(rèn)參數(shù)的好:
fs = [lambda x: x+i for i in range(3)] print([f(2) for f in fs]) fs = [lambda x, i=i: x+i for i in range(3)] print([f(2) for f in fs])
[4, 4, 4] [2, 3, 4]
另外,就是列表解析里面的作用域是一個(gè)全新的作用域,和普通的 for 循環(huán)則有所不同:
#---CASE4
fs = [lambda j:i*j for i in range(3)]
print([f(2) for f in fs])
i = 4
print([f(2) for f in fs])
#---CASE5
fs = []
for i in range(3):
fs.append(lambda j:i*j)
print([f(2) for f in fs])
i = 4
print([f(2) for f in fs])
[10, 10, 10] [10, 10, 10] [10, 10, 10] [8, 8, 8]
4.使用場景
裝飾器
惰性求值,比較常見的是在數(shù)據(jù)庫訪問的時(shí)候,可參考 Django 的 queryset 的實(shí)現(xiàn)
需要對某個(gè)函數(shù)的參數(shù)提前賦值的情況;當(dāng)然也可以使用 functools.parial 的偏函數(shù):functools.partial(func, *args, **kw),返回一個(gè) partial 函數(shù)對象。
# y = a*x + b, a 和 b 可能只出現(xiàn)一次, x 會(huì)出現(xiàn)多次
def line(a, b, x):
return a*x + b
print(line(3, 4, 5))
print(line(3, 4, 6))
print(line(7, 4, 5))
print(line(7, 4, 6))
# 2.使用閉包
def line(a, b):
def value(x):
return a*x + b
return value
# y = 3x + 4
line1 = line(3, 4)
print(line1(5))
print(line1(6))
print(line1(7))
# y = 9x + 7
line2 = line(9, 7)
print(line2(5))
print(line2(6))
print(line2(7))
# 3.使用 functools.partial 偏函數(shù)
from functools import partial
line3 = partial(line, 3)
print(line3) # functools.partial(<function line at 0x011237C8>, 3)
print(line3(4, 5))
line4 = partial(line, 3, 4)
print(line4(5))
print(line4(6))
print(line4(7))
line5 = partial(line, 9, 7)
print(line5(5))
print(line5(6))
print(line5(7))
簡單總結(jié)functools.partial的作用就是:其能把一個(gè)函數(shù)的某些參數(shù)給固定?。ㄒ簿褪窃O(shè)置默認(rèn)值),并返回一個(gè)新的函數(shù),調(diào)用這個(gè)新函數(shù)會(huì)更簡單。
總結(jié)
到此這篇關(guān)于Python中閉包和自由變量的文章就介紹到這了,更多相關(guān)Python閉包和自由變量內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python圖像處理庫PIL的ImageGrab模塊介紹詳解
這篇文章主要介紹了Python圖像處理庫PIL的ImageGrab模塊介紹詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
Python實(shí)現(xiàn)subprocess執(zhí)行外部命令
Python使用最廣泛的是標(biāo)準(zhǔn)庫的subprocess模塊,使用subprocess最簡單的方式就是用它提供的便利函數(shù),因此執(zhí)行外部命令優(yōu)先使用subprocess模塊,下面就一起來了解一下如何使用2021-05-05
在VSCode中搭建Python開發(fā)環(huán)境并進(jìn)行調(diào)試
這篇文章介紹了在VSCode中搭建Python開發(fā)環(huán)境并進(jìn)行調(diào)試的方法,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
在Python反編譯中批量pyc轉(zhuǎn)?py的實(shí)現(xiàn)代碼
這篇文章主要介紹了在Python反編譯中批量pyc轉(zhuǎn)?py的實(shí)現(xiàn)代碼,代碼簡單易懂,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02
MySQLdb ImportError: libmysqlclient.so.18解決方法
這篇文章主要介紹了MySQLdb ImportError: libmysqlclient.so.18解決方法,需要的朋友可以參考下2014-08-08
python實(shí)現(xiàn)文件批量編碼轉(zhuǎn)換及注意事項(xiàng)
本文通過實(shí)例代碼給大家介紹了python實(shí)現(xiàn)文件批量編碼轉(zhuǎn)換及注意事項(xiàng),非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2019-10-10
python如何遍歷指定路徑下所有文件(按按照時(shí)間區(qū)間檢索)
這篇文章主要給大家介紹了關(guān)于python如何遍歷指定路徑下所有文件(按按照時(shí)間區(qū)間檢索)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
詳解python 條件語句和while循環(huán)的實(shí)例代碼
這篇文章主要介紹了詳解python 條件語句和while循環(huán),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12

