閉包是Python裝飾器的基礎。要理解閉包,先要了解Python中的變量作用域規則。
變量作用域規則
首先,在函數中是能訪問全局變量的:
>>> a = 'global var'
>>> def foo():
print(a)
>>> foo()
global var
然后,在一個嵌套函數中,內層函數能夠訪問在外層函數中定義的局部變量:
>>> def foo():
a = 'free var'
def bar():
print(a)
return bar
>>> foo()()
free var
閉包
上面的嵌套函數就是閉包。閉包是指延伸了作用域的函數,在其中能夠訪問未在函數定義體中定義的非全局變量。未在函數定義體中定義的非全局變量一般都是在嵌套函數中出現的。
上述示例中的變量a就是一個並未在函數bar中定義的非全局變量。對於bar來說,它有個專業名字,叫做自由變量。
自由變量的名稱可以在字節碼對象中查看:
>>> bar = foo()
>>> bar.__code__.co_freevars
('a',)
自由變量的值綁定在函數的__closure__屬性中:
>>> bar.__closure__
(<cell at 0x000001CB2912DF48: str object at 0x000001CB291D3D70>,)
其中保存了對應自由變量的cell對象的序列,cell對象的cell_contents屬性保存了變量的值:
>>> bar.__closure__[0].cell_contents
'free var'
這與JavaScript中閉包的行為是類似的,JavaScript中嵌套函數會將外層函數的活動對象添加到它的作用域鏈中。但與JavaScript不同的是,當Python函數中的全局變量或者自由變量是不可變對象(數字、字符串、元組等)時,是只能讀取,無法更新的:
>>> a = 1
>>> def foo():
print(a)
a += 1
>>> foo()
UnboundLocalError: local variable 'a' referenced before assignment
>>> def foo():
a = 1
def bar():
print(a)
a += 1
return bar
>>> foo()()
UnboundLocalError: local variable 'a' referenced before assignment
兩種情況下,都會報錯。這並不是缺陷,而是Python的設計選擇。Python不要求聲明變量,但是會假定在函數定義體中賦值的變量是局部變量,以避免在不知情的情況下修改全局變量。
a += 1
與a = a + 1
相同,編譯函數的定義體時,會將a當做局部變量,不會當做自由變量保存。然后嘗試獲取a的值時,發現a並沒有綁定值,於是報錯。
解決這個問題的辦法,一是將變量置於一些可變對象,如列表、字典中:
def foo():
ns = {}
ns['a'] = 1
def bar():
ns['a'] += 1
print (ns['a'])
return bar
另外的方法就是使用global或者nonlocal將變量聲明為全局變量或者自由變量:
>>> def foo():
a = 1
def bar():
nonlocal a
a += 1
print(a)
return bar
>>> foo()()
2
當自由變量本身是可變對象時,是可以直接進行操作的:
def make_avg():
ls = []
def avg(x):
ls.append(x)
print(sum(ls)/len(ls))
return avg
裝飾器
裝飾器是可調用對象,參數一般是另一個函數。裝飾器可以以某種方式增強被裝飾函數的行為,然后返回被裝飾的函數或者將其替換成一個新的函數。
一個最簡單的不做任何額外行為的裝飾器:
def decorate(func):
return func
decorate
函數就是一個最簡單的裝飾器,使用方法:
def target():
pass
target = decorate(target)
Python為裝飾器的使用提供了語法糖,可以簡便的寫為:
@decorate
def target():
pass
導入時運行
裝飾器一個很重要的特性是它是導入時(加載模塊時)運行的:
def decorate(func):
print('running decorator when import')
return func
@decorate
def foo():
print('running foo')
pass
if __name__ == '__main__':
print('start foo')
foo()
結果:
running decorator when import
start foo
running foo
可以看到,裝飾器是導入時運行的,而被裝飾的函數是明確調用時運行的。
裝飾器可以返回被裝飾的函數本身,和運行時導入的特性結合起來,可以實現簡單的注冊器功能:
view_registry = []
def register(func):
view_registry.append(func)
return func
@register
def view1():
pass
@register
def view2():
pass
def main():
print(view_registry)
if __name__ == '__main__':
main()
返回新函數
上述裝飾器的例子都返回了被裝飾的原函數,但裝飾器的典型行為還是返回一個新函數:把被裝飾的函數替換成新函數,新函數接受與原函數相同的參數,並且返回原函數本該返回的值。寫法類似於:
def deco(func):
def new_func(*args, **kwargs):
return func(*args, **kwargs)
return new_func
這種情況下裝飾器就使用到了閉包。JavaScript中的防抖與節流函數就是這種典型的裝飾器行為。新函數一般會使用外部裝飾器函數中的變量當做自由變量,對函數作出某種增強行為。
舉個例子,我們知道,當Python函數的參數是個可變對象時,會產生意料之外的行為:
def foo(x, y=[]):
y.append(x)
print(y)
foo(1)
foo(2)
foo(3)
輸出:
[1]
[1, 2]
[1, 2, 3]
這是因為,函數的參數默認值保存在__defaults__屬性中,指向了同一個列表:
>>> foo.__defaults__
([1, 2, 3],)
我們就可以用一個裝飾器在函數執行前取出默認值做深復制,然后覆蓋函數原先的參數默認值:
import copy
def fresh_defaults(func):
defaults = func.__defaults__
def deco(*args, **kwargs):
func.__defaults__ = copy.deepcopy(defaults)
return func(*args, **kwargs)
return deco
@fresh_defaults
def foo(x, y=[]):
y.append(x)
print(y)
foo(1)
foo(2)
foo(3)
輸出:
[1]
[2]
[3]
接收參數的裝飾器
裝飾器除了可以接受函數作為參數外,還可以接受其他參數。使用方法是:創建一個裝飾器工廠,接受參數,返回一個裝飾器,再把它應用到被裝飾的函數上,語法如下:
def deco_factory(*args, **kwargs):
def deco(func):
print(args)
return func
return deco
@deco_factory('factory')
def foo():
pass
在Web框架中,通常要將URL模式映射到生成響應的view函數,並將view函數注冊到某些中央注冊處。之前我們曾經實現過一個簡單的注冊裝飾器,只是注冊了view函數,卻沒有URL映射,是遠遠不夠的。
在Flask中,注冊view函數需要一個裝飾器:
@app.route('/hello')
def hello():
return 'Hello, World'
原理就是使用了裝飾器工廠,可以簡單的模擬一下實現:
class App:
def __init__(self):
self.view_functions = {}
def route(self, rule):
def deco(view_func):
self.view_functions[rule] = view_func
return view_func
return deco
app = App()
@app.route('/')
def index():
pass
@app.route('/hello')
def hello():
pass
for rule, view in app.view_functions.items():
print(rule, ':', view.__name__)
輸出:
/ : index
/hello : hello
還可以使用裝飾器工廠來確定view函數可以允許哪些HTTP請求方法:
def action(methods):
def deco(view):
view.allow_methods = [method.lower() for method in methods]
return view
return deco
@action(['GET', 'POST'])
def view(request):
if request.method.lower() in view.allow_methods:
...
重疊的裝飾器
裝飾器也是可以重疊使用的:
@d1
@d2
def foo():
pass
等同於:
foo = d1(d2(foo))
類裝飾器
裝飾器的參數也可以是一個類,也就是說,裝飾器可以裝飾類:
import types
def deco(cls):
for key, method in cls.__dict__.items():
if isinstance(method, types.FunctionType):
print(key, ':', method.__name__)
return cls
@deco
class Test:
def __init__(self):
pass
def foo(self):
pass