基于Python函數(shù)的作用域規(guī)則和閉包(詳解)
作用域規(guī)則
命名空間是從名稱到對象的映射,Python中主要是通過字典實現(xiàn)的,主要有以下幾個命名空間:
內(nèi)置命名空間,包含一些內(nèi)置函數(shù)和內(nèi)置異常的名稱,在Python解釋器啟動時創(chuàng)建,一直保存到解釋器退出。內(nèi)置命名實際上存在于一個叫__builtins__的模塊中,可以通過globals()['__builtins__'].__dict__查看其中的內(nèi)置函數(shù)和內(nèi)置異常。
全局命名空間,在讀入函數(shù)所在的模塊時創(chuàng)建,通常情況下,模塊命名空間也會一直保存到解釋器退出??梢酝ㄟ^內(nèi)置函數(shù)globals()查看。
局部命名空間,在函數(shù)調(diào)用時創(chuàng)建,其中包含函數(shù)參數(shù)的名稱和函數(shù)體內(nèi)賦值的變量名稱。在函數(shù)返回或者引發(fā)了一個函數(shù)內(nèi)部沒有處理的異常時刪除,每個遞歸調(diào)用有它們自己的局部命名空間??梢酝ㄟ^內(nèi)置函數(shù)locals()查看。
python解析變量名的時候,首先搜索局部命名空間。如果沒有找到匹配的名稱,它就會搜索全局命名空間。如果解釋器在全局命名空間中也找不到匹配值,最終會檢查內(nèi)置命名空間。如果仍然找不到,就會引發(fā)NameError異常。
不同命名空間內(nèi)的名稱絕對沒有任何關(guān)系,比如:
a = 42 def foo(): a = 13 print "globals: %s" % globals() print "locals: %s" % locals() return a foo() print "a: %d" % a
結(jié)果:
globals: {'a': 42, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'C:\\Users\\h\\Desktop\\test4.py', '__package__': None, '__name__': '__main__', 'foo': <function foo at 0x0000000002C17AC8>, '__doc__': None} locals: {'a': 13} a: 42
可見在函數(shù)中對變量a賦值會在局部作用域中創(chuàng)建一個新的局部變量a,外部具有相同命名的那個全局變量a不會改變。
在Python中賦值操作總是在最里層的作用域,賦值不會復(fù)制數(shù)據(jù),只是將命名綁定到對象。刪除也是如此,比如在函數(shù)中運行del a,也只是從局部命名空間中刪除局部變量a,全局變量a不會發(fā)生任何改變。
如果使用局部變量時還沒有給它賦值,就會引發(fā)UnboundLocalError異常:
a = 42 def foo(): a += 1 return a foo()
上述函數(shù)中定義了一個局部變量a,賦值語句a += 1會嘗試在a賦值之前讀取它的值,但全局變量a是不會給局部變量a賦值的。
要想在局部命名空間中對全局變量進行操作,可以使用global語句,global語句明確地將變量聲明為屬于全局命名空間:
a = 42 def foo(): global a a = 13 print "globals: %s" % globals() print "locals: %s" % locals() return a foo() print "a: %d" % a
輸出:
globals: {'a': 13, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'C:\\Users\\h\\Desktop\\test4.py', '__package__': None, '__name__': '__main__', 'foo': <function foo at 0x0000000002B87AC8>, '__doc__': None} locals: {} a: 13
可見全局變量a發(fā)生了改變。
Python支持嵌套函數(shù)(閉包),但python 2只支持在最里層的作用域和全局命名空間中給變量重新賦值,內(nèi)部函數(shù)是不可以對外部函數(shù)中的局部變量重新賦值的,比如:
def countdown(start): n = start def display(): print n def decrement(): n -= 1 while n > 0: display() decrement() countdown(10)
運行會報UnboundLocalError異常,python 2中,解決這個問題的方法是把變量放到列表或字典中:
def countdown(start): alist = [] alist.append(start) def display(): print alist[0] def decrement(): alist[0] -= 1 while alist[0] > 0: display() decrement() countdown(10)
在python 3中可以使用nonlocal語句解決這個問題,nonlocal語句會搜索當(dāng)前調(diào)用棧中的下一層函數(shù)的定義。:
def countdown(start): n = start def display(): print n def decrement(): nonlocal n n -= 1 while n > 0: display() decrement() countdown(10)
閉包
閉包(closure)是函數(shù)式編程的重要的語法結(jié)構(gòu),Python也支持這一特性,舉例一個嵌套函數(shù):
def foo(): x = 12 def bar(): print x return bar foo()()
輸出:12
可以看到內(nèi)嵌函數(shù)可以訪問外部函數(shù)定義的作用域中的變量,事實上內(nèi)嵌函數(shù)解析名稱時首先檢查局部作用域,然后從最內(nèi)層調(diào)用函數(shù)的作用域開始,搜索所有調(diào)用函數(shù)的作用域,它們包含非局部但也非全局的命名。
組成函數(shù)的語句和語句的執(zhí)行環(huán)境打包在一起,得到的對象就稱為閉包。在嵌套函數(shù)中,閉包將捕捉內(nèi)部函數(shù)執(zhí)行所需要的整個環(huán)境。
python函數(shù)的code對象,或者說字節(jié)碼中有兩個和閉包有關(guān)的對象:
co_cellvars: 是一個元組,包含嵌套的函數(shù)所引用的局部變量的名字
co_freevars: 是一個元組,保存使用了的外層作用域中的變量名
再看下上面的嵌套函數(shù):
>>> def foo(): x = 12 def bar(): return x return bar >>> foo.func_code.co_cellvars ('x',) >>> bar = foo() >>> bar.func_code.co_freevars ('x',)
可以看出外層函數(shù)的code對象的co_cellvars保存了內(nèi)部嵌套函數(shù)需要引用的變量的名字,而內(nèi)層嵌套函數(shù)的code對象的co_freevars保存了需要引用外部函數(shù)作用域中的變量名字。
在函數(shù)編譯過程中內(nèi)部函數(shù)會有一個閉包的特殊屬性__closure__(func_closure)。__closure__屬性是一個由cell對象組成的元組,包含了由多個作用域引用的變量:
>>> bar.func_closure (<cell at 0x0000000003512C78: int object at 0x0000000000645D80>,)
若要查看閉包中變量的內(nèi)容:
>>> bar.func_closure[0].cell_contents 12
如果內(nèi)部函數(shù)中不包含對外部函數(shù)變量的引用時,__closure__屬性是不存在的:
>>> def foo(): x = 12 def bar(): pass return bar >>> bar = foo() >>> print bar.func_closure None
當(dāng)把函數(shù)當(dāng)作對象傳遞給另外一個函數(shù)做參數(shù)時,再結(jié)合閉包和嵌套函數(shù),然后返回一個函數(shù)當(dāng)做返回結(jié)果,就是python裝飾器的應(yīng)用啦。
延遲綁定
需要注意的一點是,python函數(shù)的作用域是由代碼決定的,也就是靜態(tài)的,但它們的使用是動態(tài)的,是在執(zhí)行時確定的。
>>> def foo(n): return n * i >>> fs = [foo for i in range(4)] >>> print fs[0](1)
當(dāng)你期待結(jié)果是0的時候,結(jié)果卻是3。
這是因為只有在函數(shù)foo被執(zhí)行的時候才會搜索變量i的值, 由于循環(huán)已結(jié)束, i指向最終值3, 所以都會得到相同的結(jié)果。
在閉包中也存在相同的問題:
def foo(): fs = [] for i in range(4): fs.append(lambda x: x*i) return fs for f in foo(): print f(1)
返回:
解決方法,一個是為函數(shù)參數(shù)設(shè)置默認(rèn)值:
>>> fs = [lambda x, i=i: x * i for i in range(4)] >>> for f in fs: print f(1)
另外就是使用閉包了:
>>> def foo(i): return lambda x: x * i >>> fs = [foo(i) for i in range(4)] >>> for f in fs: print f(1)
或者:
>>> for f in map(lambda i: lambda x: i*x, range(4)): print f(1)
使用閉包就很類似于偏函數(shù)了,也可以使用偏函數(shù):
>>> fs = [functools.partial(lambda x, i: x * i, i) for i in range(4)] >>> for f in fs: print f(1)
這樣自由變量i都會優(yōu)先綁定到閉包函數(shù)上。
以上這篇基于Python函數(shù)的作用域規(guī)則和閉包(詳解)就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
淺談算法之最小生成樹Kruskal的Python實現(xiàn)
最小生成樹Kruskal算法可以稱為“加邊法”,初始最小生成樹邊數(shù)為0,每迭代一次就選擇一條滿足條件的最小代價邊,加入到最小生成樹的邊集合里。本文將介紹它的原理,并用Python進行實現(xiàn)2021-06-06PyCharm2020.1.1與Python3.7.7的安裝教程圖文詳解
這篇文章主要介紹了PyCharm2020.1.1與Python3.7.7的安裝教程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08Python調(diào)整數(shù)組形狀如何實現(xiàn)
這篇文章主要介紹了Python調(diào)整數(shù)組形狀如何實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-12-12matlab中二維插值函數(shù)interp2的使用詳解
這篇文章主要介紹了matlab中二維插值函數(shù)interp2的使用詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-04-04Python調(diào)用Pandas實現(xiàn)Excel讀取
這篇文章主要為大家介紹了在Python中如何調(diào)用Pandas實現(xiàn)Excel文件的讀取,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-04-04Python?"手繪風(fēng)格"數(shù)據(jù)可視化方法實例匯總
這篇文章主要給大家介紹了關(guān)于Python?"手繪風(fēng)格"數(shù)據(jù)可視化方法實現(xiàn)的相關(guān)資料,本文分別給大家?guī)砹薖ython-matplotlib手繪風(fēng)格圖表繪制、Python-cutecharts手繪風(fēng)格圖表繪制以及Python-py-roughviz手繪風(fēng)格圖表繪制,需要的朋友可以參考下2022-02-02利用python計算均值、方差和標(biāo)準(zhǔn)差(Numpy和Pandas)
這篇文章主要給大家介紹了關(guān)于利用python計算均值、方差和標(biāo)準(zhǔn)差的相關(guān)資料,Numpy在Python中是一個通用的數(shù)組處理包,它提供了一個高性能的多維數(shù)組對象和用于處理這些數(shù)組的工具,它是使用Python進行科學(xué)計算的基礎(chǔ)包,需要的朋友可以參考下2023-11-11