Python裝飾器與閉包

閉包是Python裝飾器的基礎。要理解閉包,先要了解Python中的變量做用域規則。python

變量做用域規則

首先,在函數中是能訪問全局變量的:閉包

>>> a = 'global var'

>>> def foo():
	print(a)

>>> foo()
global var

而後,在一個嵌套函數中,內層函數可以訪問在外層函數中定義的局部變量:app

>>> def foo():
	a = 'free var'
	def bar():
	    print(a)
	return bar

>>> foo()()
free var

閉包

上面的嵌套函數就是閉包。閉包是指延伸了做用域的函數,在其中可以訪問未在函數定義體中定義的非全局變量。未在函數定義體中定義的非全局變量通常都是在嵌套函數中出現的。框架

上述示例中的變量a就是一個並未在函數bar中定義的非全局變量。對於bar來講,它有個專業名字,叫作自由變量函數

自由變量的名稱能夠在字節碼對象中查看:spa

>>> bar = foo()
>>> bar.__code__.co_freevars
('a',)

自由變量的值綁定在函數的__closure__屬性中:設計

>>> bar.__closure__
(<cell at 0x000001CB2912DF48: str object at 0x000001CB291D3D70>,)

其中保存了對應自由變量的cell對象的序列,cell對象的cell_contents屬性保存了變量的值:code

>>> 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不要求聲明變量,可是會假定在函數定義體中賦值的變量是局部變量,以免在不知情的狀況下修改全局變量。ip

a += 1a = 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
相關文章
相關標籤/搜索