您好,登錄后才能下訂單哦!
小編給大家分享一下Python中作用域規則和閉包的示例分析,希望大家閱讀完這篇文章之后都有所收獲,下面讓我們一起去探討吧!
Python中的作用域
假設在交互式命令行中定義如下的函數:
>>> a = 1 >>> def foo(): b = 2 c = 3 print "locals: %s" % locals() return "result: %d" % (a + b +c) >>> a = 1 >>> def foo(): b = 2 c = 3 print "locals: %s" % locals() return "result: %d" % (a + b +c)
上述代碼先給a賦值1,緊接著定義了一個函數:foo()。在函數foo()中我們定義了兩個整數b和c,函數的返回值為a、b、c三個數的和。
對上述函數進行驗證:
# result >>> foo() locals: {'c': 3, 'b': 2} result: 6 # result >>> foo() locals: {'c': 3, 'b': 2} result: 6
根據驗證的結果,foo()函數的返回值為6。上述的函數定義中只有b和c兩個變量的賦值,那調用函數是如何判斷a的值呢?這涉及到函數的作用域規則。本文摘錄《Python參考手冊(第4版)》中的相關論述:
每次執行一個函數時, 就會創建心得局部命名空間。該命名空間代表一個局部環境,其中包含函數參數的名稱和在函數體內賦值的變量名稱。解析這些名稱時:
解釋器將首先搜索局部命名空間;
如果沒有找到匹配的名稱,它就會搜索全局命名空間(函數的全局命名空間始終是定義該函數的模塊);
如果解釋器在全局命名空間中也找不到匹配值,最終會檢查內置命名空間;
如果在內置命名空間中也找不到匹配值,就會引發NameError異常。
對應于上面的例子,foo函數首先會在局部命名空間中找三個變量的匹配值。上述代碼中的locals()方法給出了foo函數局部命名空間的內容。可以看出,局部命名空間是一個字典,包含b和c的值,這是因為我們在foo函數中定義了這兩個變量。然而,局部命名空間中不包含a的值,所以就需要在全局命名空間中尋找。可以使用__globals__獲取一個函數的局部命名空間。
# foo函數的全局命名空間 >>> foo.__globals__ {'a': 1, '__builtins__': <module '__builtin__' (built-in)>, '__package__': None, '__name__': '__main__', 'foo': <function foo at 0x0000000004613518>, '__doc__': None} # foo函數的全局命名空間 >>> foo.__globals__ {'a': 1, '__builtins__': <module '__builtin__' (built-in)>, '__package__': None, '__name__': '__main__', 'foo': <function foo at 0x0000000004613518>, '__doc__': None}
foo函數的全局命名空間中包含了內置函數模塊、foo函數、變量a以及其他的一些參數。由于在foo函數的全局命名空間中找到了變量a,foo函數便返回三個變量的和。
Python閉包
上述的Python作用域規則具有普遍性。然而,在Python中“一切皆對象”,函數也不例外。這也就是說可以把函數當作參數傳遞給其他的函數,也可以放在數據結構中,還可以作為函數的返回結果。在這種情況下,Python的作用域規則會發生什么變化呢?我們還是舉一個例子:
>>> def foo(): a = 1 def bar(): b = 2 c = 3 return a + b + c return bar >>> def foo(): a = 1 def bar(): b = 2 c = 3 return a + b + c return bar
在這個例子中,我們定義了一個函數foo,并對變量a賦值。不過與之前的例子不同的是,在函數foo中我們還嵌套了一個函數bar,并且還定義了兩個變量,這個函數是作為函數foo的返回值。根據上面的作用域規則,函數foo的局部作用域既不是函數bar的局部作用域,也不是它的全局作用域,那函數bar能否正確匹配變量a的值呢?我們我們來驗證一下這個函數是否能夠正常運行。
# 調用函數foo() >>> bar = foo() # 返回值bar是一個函數 >>> bar <function bar at 0x00000000045F3588> # 調用bar() >>> bar() # 結果顯示為三個變量之和 6
以上的驗證結果說明,在上述嵌套的函數中,內部函數可以正確地引用外部函數的變量,即使外部的函數已經返回。
這種內部函數的局部作用域中可以訪問外部函數局部作用域中變量的行為,我們稱為: 閉包。內部函數可以訪問外部函數變量的特點很像將外部函數的變量直接“打包”到內部函數中一樣,我們也可以這樣理解閉包:將組成函數的語句以及執行這些語句的環境“打包”在一起時得到的對象稱為閉包。
和閉包相關的幾個對象
為了了解閉包是怎么實現內部函數對外部函數變量的引用,還需要對閉包相關的幾個對象進行介紹。關于這幾個對象會涉及到Python的底層實現,本文中對此不加以詳述,可以參考以下文章:
不過,為了直觀地說明閉包的實現過程(不分析底層實現),這里先簡單介紹以下code對象。code對象是指代碼對象,表示編譯成字節的的可執行Python代碼,或者字節碼。它有幾個比較重要的屬性:
co_name:函數的名稱
co_nlocals: 函數使用的局部變量的個數
co_varnames: 一個包含局部變量名字的元組
co_cellvars: 是一個元組,包含嵌套的函數所引用的局部變量的名字
co_freevars: 是一個元組,保存使用了的外層作用域中的變量名
co_consts: 是一個包含字節碼使用的字面量的元組
其中比較關鍵的是co_varnames和co_freevars兩個屬性。我們對上面的例子稍加修改:
Python
>>> def foo(): a = 1 b = 2 def bar(): return a + 1 def bar2(): return b + 2 return bar >>> bar = foo() # 外層函數 >>> foo.func_code.co_cellvars ('a', 'b') >>> foo.func_code.co_freevars () # 內層嵌套函數 >>> bar.func_code.co_cellvars () >>> bar.func_code.co_freevars ('a',) >>> def foo(): a = 1 b = 2 def bar(): return a + 1 def bar2(): return b + 2 return bar >>> bar = foo() # 外層函數 >>> foo.func_code.co_cellvars ('a', 'b') >>> foo.func_code.co_freevars () # 內層嵌套函數 >>> bar.func_code.co_cellvars () >>> bar.func_code.co_freevars ('a',)
以上說明外層函數的code對象的co_cellvars保存了內部嵌套函數需要引用的變量的名字,而內層嵌套函數的code對象的co_freevars保存了需要引用外部函數作用域中的變量名字。具體來說,就是foo函數中嵌套了兩個函數,它們都需要引用foo函數局部作用域中的變量,所以foo.func_code.co_cellvars便包含變量a和變量b的名稱。而函數bar是foo的返回值,只引用了變量a,因此bar.func_code.co_freevars中便只包含變量a。
內部函數和外部函數的co_freevars、co_cellvars的對應關系,使得在函數編譯過程中內部函數具有了一個閉包的特殊屬性__closure__(底層中對此有相關實現)。__closure__屬性是一個由cell對象組成的元組,包含了由多個作用域引用的變量。可以做以下驗證:
>>> foo.__closure__ #None # 內部函數bar對變量a的引用 >>> bar.__closure__ (<cell at 0x00000000044F6798: int object at 0x0000000003FA4B38>,) # 內部函數bar引用的變量a的值 >>> bar.__closure__[0].cell_contents 1
看完了這篇文章,相信你對“Python中作用域規則和閉包的示例分析”有了一定的了解,如果想了解更多相關知識,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。