作用域規則
命名空間是從名稱到對象的映射,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都會優先綁定到閉包函數上。