淺談Python中的做用域規則和閉包

在對Python中的閉包進行簡單分析以前,咱們先了解一下Python中的做用域規則。關於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三個數的和。ui

對上述函數進行驗證:命令行

# 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版)》中的相關論述:code

每次執行一個函數時, 就會建立心得局部命名空間。該命名空間表明一個局部環境,其中包含函數參數的名稱和在函數體內賦值的變量名稱。解析這些名稱時:對象

解釋器將首先搜索局部命名空間;作用域

若是沒有找到匹配的名稱,它就會搜索全局命名空間(函數的全局命名空間始終是定義該函數的模塊);io

若是解釋器在全局命名空間中也找不到匹配值,最終會檢查內置命名空間;編譯

若是在內置命名空間中也找不到匹配值,就會引起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
相關文章
相關標籤/搜索