Python函數的做用域規則和閉包

做用域規則

命名空間是從名稱到對象的映射,Python中主要是經過字典實現的,主要有如下幾個命名空間:python

  • 內置命名空間,包含一些內置函數和內置異常的名稱,在Python解釋器啓動時建立,一直保存到解釋器退出。內置命名實際上存在於一個叫__builtins__的模塊中,能夠經過globals()['__builtins__'].__dict__查看其中的內置函數和內置異常。
  • 全局命名空間,在讀入函數所在的模塊時建立,一般狀況下,模塊命名空間也會一直保存到解釋器退出。能夠經過內置函數globals()查看。
  • 局部命名空間,在函數調用時建立,其中包含函數參數的名稱和函數體內賦值的變量名稱。在函數返回或者引起了一個函數內部沒有處理的異常時刪除,每一個遞歸調用有它們本身的局部命名空間。能夠經過內置函數locals()查看。

python解析變量名的時候,首先搜索局部命名空間。若是沒有找到匹配的名稱,它就會搜索全局命名空間。若是解釋器在全局命名空間中也找不到匹配值,最終會檢查內置命名空間。若是仍然找不到,就會引起NameError異常。編程

不一樣命名空間內的名稱絕對沒有任何關係,好比:bash

a = 42
def foo():
    a = 13
    print "globals: %s" % globals()
    print "locals: %s" % locals()
    return a
foo()
print "a: %d" % a

結果:閉包

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

可見在函數中對變量a賦值會在局部做用域中建立一個新的局部變量a,外部具備相同命名的那個全局變量a不會改變。app

在Python中賦值操做老是在最裏層的做用域,賦值不會複製數據,只是將命名綁定到對象。刪除也是如此,好比在函數中運行del a,也只是從局部命名空間中刪除局部變量a,全局變量a不會發生任何改變。
函數式編程

若是使用局部變量時尚未給它賦值,就會引起UnboundLocalError異常:函數

a = 42
def foo():
    a += 1
    return a
foo()

上述函數中定義了一個局部變量a,賦值語句a += 1會嘗試在a賦值以前讀取它的值,但全局變量a是不會給局部變量a賦值的。ui

要想在局部命名空間中對全局變量進行操做,可使用global語句,global語句明確地將變量聲明爲屬於全局命名空間:spa

a = 42
def foo():
    global a
    a = 13
    print "globals: %s" % globals()
    print "locals: %s" % locals()
    return a
foo()
print "a: %d" % a

輸出:code

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發生了改變。

Python支持嵌套函數(閉包),但python 2只支持在最裏層的做用域和全局命名空間中給變量從新賦值,內部函數是不能夠對外部函數中的局部變量從新賦值的,好比:

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語句會搜索當前調用棧中的下一層函數的定義。:

def countdown(start):
    n = start
    def display():
        print n
    def decrement():
        nonlocal n
        n -= 1
    while n > 0:
        display()
        decrement()
countdown(10)

 閉包

閉包(closure)是函數式編程的重要的語法結構,Python也支持這一特性,舉例一個嵌套函數:

def foo():
    x = 12
    def bar():
        print x
    return bar
foo()()

輸出:12

能夠看到內嵌函數能夠訪問外部函數定義的做用域中的變量,事實上內嵌函數解析名稱時首先檢查局部做用域,而後從最內層調用函數的做用域開始,搜索全部調用函數的做用域,它們包含非局部但也非全局的命名。

組成函數的語句和語句的執行環境打包在一塊兒,獲得的對象就稱爲閉包。在嵌套函數中,閉包將捕捉內部函數執行所須要的整個環境。

python函數的code對象,或者說字節碼中有兩個和閉包有關的對象:

  • co_cellvars: 是一個元組,包含嵌套的函數所引用的局部變量的名字
  • co_freevars: 是一個元組,保存使用了的外層做用域中的變量名

再看下上面的嵌套函數:

>>> def foo():
	    x = 12
	    def bar():
		    return x
	    return bar

>>> foo.func_code.co_cellvars
('x',)
>>> bar = foo()
>>> bar.func_code.co_freevars
('x',)

能夠看出外層函數的code對象的co_cellvars保存了內部嵌套函數須要引用的變量的名字,而內層嵌套函數的code對象的co_freevars保存了須要引用外部函數做用域中的變量名字。

在函數編譯過程當中內部函數會有一個閉包的特殊屬性__closure__(func_closure)。__closure__屬性是一個由cell對象組成的元組,包含了由多個做用域引用的變量:

>>> bar.func_closure
(<cell at 0x0000000003512C78: int object at 0x0000000000645D80>,)

若要查看閉包中變量的內容:

>>> bar.func_closure[0].cell_contents
12

若是內部函數中不包含對外部函數變量的引用時,__closure__屬性是不存在的:

>>> def foo():
	    x = 12
	    def bar():
		    pass
	    return bar

>>> bar = foo()
>>> print bar.func_closure
None

當把函數看成對象傳遞給另一個函數作參數時,再結合閉包和嵌套函數,而後返回一個函數當作返回結果,就是python裝飾器的應用啦。

延遲綁定

須要注意的一點是,python函數的做用域是由代碼決定的,也就是靜態的,但它們的使用是動態的,是在執行時肯定的。

>>> def foo(n):
	    return n * i

>>> fs = [foo for i in range(4)]
>>> print fs[0](1)

當你期待結果是0的時候,結果倒是3。

這是由於只有在函數foo被執行的時候纔會搜索變量i的值, 因爲循環已結束, i指向最終值3, 因此都會獲得相同的結果。

在閉包中也存在相同的問題:

def foo():
    fs = []
    for i in range(4):
        fs.append(lambda x: x*i)
    return fs
for f in foo():
    print f(1)

返回:

3
3
3
3

解決方法,一個是爲函數參數設置默認值:

>>> 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)

使用閉包就很相似於偏函數了,也可使用偏函數:

>>> fs = [functools.partial(lambda x, i: x * i, i) for i in range(4)]
>>> for f in fs:
	    print f(1)

這樣自由變量i都會優先綁定到閉包函數上。

相關文章
相關標籤/搜索