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


作用域規則

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

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

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

不同命名空間內的名稱絕對沒有任何關系,比如:

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不會改變。

在Python中賦值操作總是在最里層的作用域,賦值不會復制數據,只是將命名綁定到對象。刪除也是如此,比如在函數中運行del a,也只是從局部命名空間中刪除局部變量a,全局變量a不會發生任何改變。

如果使用局部變量時還沒有給它賦值,就會引發UnboundLocalError異常:

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

上述函數中定義了一個局部變量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發生了改變。

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都會優先綁定到閉包函數上。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM