Python Closure

在計算機科學中,閉包 又稱 詞法閉包 或 函數閉包,是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即便已經離開了創造它的環境也不例外。閉包被普遍應用於函數式語言中。閉包

從上面這段話中能夠看出閉包的兩個重要條件是引用自由變量函數,與閉包這個名稱結合起來看,這個函數就像是一個包,而這個函數所引用的變量就比如函數這個包中封閉起來的東西,包中的東西被牢牢封閉在包中,函數所引用的變量也被與這個函數所綁定。app

首先來看兩個概念 Nonlocal variable 和 Nested function函數

Nonlocal variable & Nested function

Nonlocal variable是相對於某個函數來講的,指的是這個函數所調用的在本函數做用域以外的變量,Nested function指的被定義在一個函數(outer enclosing function)中的函數,這個nested function能夠調用包圍它的做用域中的變量。code

看一個例子orm

def print_msg(msg):
    # outer enclosing function

    def printer():
        # nested function
        print(msg)

    printer()

>>> print_msg("Hello")
Hello

在這個例子中函數printer就是一個nested function,而變量msg就是一個nonlocal variable。這裏須要注意的是,printer雖然能夠訪問msg,可是不能夠改變它,若是嘗試更改會出現UnboundLocalError: local variable 'msg' referenced before assignment對象

def print_msg(msg):
    def printer():
        msg += 'a'
        print(msg)
    printer()

>>> print_msg("Hello")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in print_msg
  File "<stdin>", line 3, in printer
UnboundLocalError: local variable 'msg' referenced before assignment
local variable 'msg' referenced before assignment

若是必需要更改這個變量的值,在Python3中新引入的nonlocal語句能夠解決。資源

def print_msg(msg):
    def printer():
        nonlocal msg
        msg += 'a'
        print(msg)
    printer()

>>> print_msg("Hello")
Helloa

在Python2中使用global也可解決,可是global會直接查找全局變量,而nonlocal則是按優先級從本地-->全局進行搜索。作用域

閉包函數

下面使外層函數(outer enclosing function)返回一個函數it

def print_msg(msg):
    def printer():
        print(msg)
    return printer

>>> another = print_msg("Hello")
>>> another()
Hello

print_msg("Hello")返回的函數賦值給another,再調用another函數時,發現已經離開了print_msg函數的做用域,可是"Hello"已經被綁定給another,因此仍然可以正常調用,這就是Python中的閉包。

刪除print_msg以後,another仍然可以正常調用。io

>>> del print_msg
>>> print_msg("Hello")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'print_msg' is not defined
name 'print_msg' is not defined

>>> another()
Hello

閉包的應用

當符合下面幾個條件時就造成了閉包:

  • 有一個Nested function

  • 這個Nested function訪問了父函數做用域中的變量

  • 父函數返回了這個Nested function

閉包主要運用在須要講父函數做用域中的變量綁定到子函數的場景之中,在釋放掉父函數以後子函數也不會受到影響。運用閉包能夠避免對全局變量的使用。對於一個只有須要實現少數方法的類咱們也能夠用閉包來替代,這樣作能夠減小資源的使用。

下面須要用類定義不一樣動物的叫聲

class Animal:
    def __init__(self, animal):
        self.animal = animal
    def sing(self, voice):
        return "{} sings {}".format(self.animal, voice)

>>> dog = Animal("dog")
>>> cow = Animal("cow")
>>> dog.sing("wong")
'dog sings wong'
>>> cow.sing("mow")
cow sings mow'

用閉包替代

def make_sing(animal):
    def make_voice(voice):
        return "{} sings {}".format(animal, voice)
    return make_voice

>>> dog = make_sing("dog")
>>> dog("wong")
'dog sings wong'
>>> cow = make_sing("cow")
>>> cow("mow")
'cow sings mow'

閉包與裝飾器

閉包一般用來實現一個通用的功能,Python中的裝飾器就是對閉包的一種應用,只不過裝飾器中父函數的參數是一個函數,下面這個例子經過裝飾器實現了在子函數執行先後輸出提示信息。

def make_wrap(func):
    def wrapper(*args):
        print("before function")
        func(*args)
        print("after function")
    return wrapper

@make_wrap
def print_msg(msg):
    print(msg)

>>> print_msg("Hello")
before function
Hello
after function

裝飾器也能夠進行疊加

def make_another(func):
    def wrapper(*args):
        print("another begin")
        func(*args)
        print("another end")
    return wrapper

@make_another
@make_wrap
def print_msg(msg):
    print(msg)

>>> print_msg("Hello")
another begin
before function
Hello
after function
another end

閉包的內部實現

Code Object

爲了瞭解閉包的內部實現,須要用compile命令得出相應的code object

>>> code_obj = compile("print_msg('Hello')", "", "single")

這裏第一個參數是一個能夠被execeval解析的模塊、語句或者表達式;

第二個參數是用來存放運行時錯誤的文件;

第三個選擇single模式,與前面第一個參數填寫的表達式相匹配,若是第一個參數是表達式則須要用eval模式,若是是模塊則應該用exec模式。

下面經過discode_obj反編譯成助記符

>>> dis.dis(code_obj)
  1           0 LOAD_NAME                0 (print_msg)
              2 LOAD_CONST               0 ('Hello')
              4 CALL_FUNCTION            1
              6 PRINT_EXPR
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE

Python3中經過__code__訪問函數的code object(Python2中爲func_code)

>>> print_msg.__code__
<code object print_msg at 0x10d5c7300, file "<stdin>", line 1>

Cell Object

cell object用來存儲被多個做用域所引用的變量。

好比下面函數中msgprint_msg所引用,也被printer所引用,因此msg會被存在一個cell object

def print_msg(msg):
    def printer():
        print(msg)
    return printer

查看其__closure__屬性能夠驗證咱們的想法

>>> print_msg("Hello").__closure__
(<cell at 0x10d121d38: str object at 0x10d4a6f48>,)

儘管這兩個引用都被存在贊成個cell object,可是他們仍然只在各自的做用域下做用。

閉包分析

首先反編譯print_msg

>>> dis.dis(print_msg)
  2           0 LOAD_CLOSURE             0 (msg)
              2 BUILD_TUPLE              1
              4 LOAD_CONST               1 (<code object printer at 0x10d5c7780, file "<stdin>", line 2>)
              6 LOAD_CONST               2 ('print_msg.<locals>.printer')
              8 MAKE_FUNCTION            8
             10 STORE_FAST               1 (printer)

  4          12 LOAD_FAST                1 (printer)
             14 RETURN_VALUE
  • LOAD_CLOSURE 0 (msg)將變量msg進棧。

  • BUILD_TUPLE 1 將棧頂的元素取出,建立元組,並將該元組push進棧。

  • LOAD_CONST 1print_msg.__code__.co_consts[1]中取出,爲printercode object的地址,將其push進棧。

  • LOAD_CONST 2print_msg.__code__.co_consts[2]中取出,將其push進棧。

  • STORE_FAST 1從棧頂取出以前建立的函數對象的地址信息賦給局部變量printer(局部變量名記錄在__code__.co_varnames中)
    __code__.co_varnames的內容爲('msg','printer')

將變量msg(記錄在__code__.co_cellvars[0])綁定棧頂的函數對象地址。

  • LOAD_FAST 1msg的值壓入棧。

  • RETURN_VALUE返回棧頂。

能夠看到在STORE_FAST 1中將變量msg綁定到了printer函數,從而達到了閉包中內部函數訪問外部函數變量的效果。

相關文章
相關標籤/搜索