函數裝飾器和閉包

在23中設計模式中,給出關於裝飾器模式的定義是:就是對已經存在的某些類進行裝飾,以此來擴展或者說加強函數的一些功能;然而在python中,裝飾器使用一種語法糖來實現。不知道你是否知道nonlocal關鍵字以及閉包,可是若是你想本身實現函數裝飾器,那就必須瞭解閉包的方方面面,所以也就須要知道 nonlocal
要理解裝飾器,就要知道下面問題:html

  • Python 如何計算裝飾器句法
  • Python 如何判斷變量是否是局部的
  • 閉包存在的緣由和工做原理
  • nonlocal 能解決什麼問題

1、裝飾器基本知識

裝飾器是可調用的對象,其參數是另外一個函數(被裝飾的函數)。 裝飾器可能會處理被裝飾的函數,而後把它返回,或者將其替換成另外一個函數或可調用對象。
活很少說來看下面例子:python

>>> def deco(func):
...     def inner():
...         print('running inner()')
...     return inner ➊
...
>>> @deco
... def target(): ➋
...     print('running target()')
...
>>> target() ➌
running inner()
>>> target ➍
<function deco.<locals>.inner at 0x10063b598>

❶ deco 返回 inner 函數對象。
❷ 使用 deco 裝飾 target。
❸ 調用被裝飾的 target 其實會運行 inner。
❹ 審查對象,發現 target 如今是 inner 的引用。
若是看完這個例子你可能會有疑問,明明上面說的是,裝飾器是加強函數的功能,可是爲何這裏卻改變了函數的打印結果,這是由於在deco(func)函數傳入的形參是func,可是後來咱們在func這個函數中卻沒有用到func而是用inner,所以target的行爲改變了。
:若是你對這裏的操縱檯的操做 target 輸出結果有疑問,或者說不知道爲何不是 target() 的話,能夠參考博主的另外一篇博文 一等函數畢竟函數在Python中也是一種對象程序員

裝飾器的一大特性是,能把被裝飾的函數替換成其餘函數。第二個特性是,裝飾器在加載模塊時當即執行,下面咱們來看一下。設計模式

2、Python什麼時候執行裝飾器

一、做爲腳本執行時

裝飾器的一個關鍵特性是,它們在被裝飾的函數定義以後當即運行。這一般是在導入時(即 Python 加載模塊時)閉包

registry = [] ➊\

def register(func): ➋
    print('running register(%s)' % func) ➌
    registry.append(func) ➍
    return func ➎

@register ➏
def f1():
    print('running f1()')

@register
def f2():
    print('running f2()')

def f3(): ➐
    print('running f3()')

def main(): ➑
    print('running main()')
    print('registry ->', registry)
    f1()
    f2()
    f3()

if __name__=='__main__':
    main() ➒

❶ registry 保存被 @register 裝飾的函數引用。
❷ register 的參數是一個函數。
❸ 爲了演示,顯示被裝飾的函數。
❹ 把 func 存入 registry。
❺ 返回 func:必須返回函數;這裏返回的函數與經過參數傳入的同樣。
❻ f1 和 f2 被 @register 裝飾。
❼ f3 沒有裝飾。
❽ main 顯示 registry,而後調用 f1()、f2() 和 f3()。
❾ 只有把 registration.py 看成腳本運行時才調用 main()app

把 registration.py 看成腳本運行獲得的輸出以下:函數

$ python3 registration.py
running register(<function f1 at 0x100631bf8>)
running register(<function f2 at 0x100631c80>)
running main()
registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
running f1()
running f2()
running f3()

經過執行腳本能夠看出,在執行main()函數以前,裝飾器都已經被調用過二次了,裝飾器的參數分別是被裝飾的兩個函數對象f1f2debug

二、做爲模塊導入時

若是導入 registration.py 模塊(不做爲腳本運行),輸出以下:設計

>>> import registration
running register(<function f1 at 0x10063b1e0>)
running register(<function f2 at 0x10063b268>)

此時查看 registry 的值,獲得的輸出以下:code

>>> registration.registry
[<function f1 at 0x10063b1e0>, <function f2 at 0x10063b268>]

函數裝飾器在導入模塊時當即執行,而被裝飾的函數只在明確調用時運行。這突出了 Python 程序員所說的導入時和運行時之間的區別。

三、總結

  • 裝飾器函數與被裝飾的函數在同一個模塊中定義。實際狀況是,裝飾器一般在一個模塊中定義,而後應用到其餘模塊中的函數上。
  • register 裝飾器返回的函數與經過參數傳入的相同。實際上,大多數裝飾器會在內部定義一個函數,而後將其返回。

3、變量做用域規則

若是你瞭解全局變量與局部變量的區別,那就很容易明白下面的問題

>>> def f1(a):
... print(a)
... print(b)
...
>>> f1(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f1
NameError: global name 'b' is not defined

錯誤提示給出了,變量b未定義。那若是這樣呢,咱們事先給變量b定義好

>>> b = 6
>>> f1(3)
3
6

就不會出錯

在來看下面這段代碼,若是你對變量做用域不夠清楚的話就會出錯。

>>> b = 6
>>> def f2(a):
... print(a)
... print(b)
... b = 9
...
>>> f2(3)
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment

我剛開始看的時候,也是一臉懵逼,可是看到流暢的Python關於這個解釋才明白。在函數體編譯的時候,因爲b=9賦值語句是在函數f2內部,因此python判斷b是一個局部變量,可是在執行函數的print(b)的時候,b變量尚未綁定任何值,因此出錯。

想要解決這一問題,就要告訴python,b變量是一個全局變量,在打印的時候纔不會出錯。要使用 global 聲明:

>>> b = 6
>>> def f3(a):
... global b
... print(a)
... print(b)
... b = 9
...
>>> f3(3)
3
6
>>> b
9
>>> f3(3)
3
9
>>> b = 30
>>> b
30

4、閉包

閉包指延伸了做用域的函數,其中包含函數定義體中引用、可是不在定義體中定義的非全局變量。函數是否是匿名的沒有關係,關鍵是它能訪問定義體以外定義的非全局變量。

class Averager():
    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)

:這裏的call特殊方法使得Averager函數對象成爲了一個能夠調用的對象。若是還有疑問能夠參考博主的另外一篇博文 一等函數
假若有個名爲 avg 的函數,它的做用是計算不斷增長的系列值的均值;例如,整個歷史中某個商品的平均收盤價。天天都會增長新價格,所以平均值要考慮至目前爲止全部的價格。
操縱檯輸入輸出:Averager 的實例是可調用對象:

>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

這樣就實現了計算平均值。固然這樣寫沒有問題,可是還有另外一種寫法。

def make_averager():
    series = []

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return averager

調用make_averager 時,返回一個 averager 函數對象。每次調用averager 時,它會把參數添加到系列值中,而後計算當前平均值。
:這裏make_averager扮演者一個高階函數的角色,所謂高階函數就是把其餘函數對象做爲參數傳入自身的函數對象。

>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

注意,這兩個示例有共通之處:調用 Averager() 或make_averager() 獲得一個可調用對象 avg,它會更新歷史值,而後計算當前均值。在第一個示例 ,avg 是 Averager 的實例;在第二個示例中是內部函數 averager。無論怎樣,咱們都只需調用 avg(n),把 n放入系列值中,而後從新計算均值。
Averager 類的實例 avg 在哪裏存儲歷史值很明顯:self.series 實例屬性。可是第二個示例中的 avg 函數在哪裏尋找 series 呢?
注意,series 是 make_averager 函數的局部變量,由於那個函數的定義體中初始化了 series:series = []。但是,調用 avg(10)時,make_averager 函數已經返回了,而它的本地做用域也一去不復返了。

在 averager 函數中,series 是自由變量(free variable)。這是一個技術術語,指未在本地做用域中綁定的變量

在圖中:averager 的閉包延伸到那個函數的做用域以外,包含自由變量 series 的綁定。

閉包是一種函數,它會保留定義函數時存在的自由變量的綁定,這樣調用函數時,雖然定義做用域不可用了,可是仍能使用那些綁定。
注意,只有嵌套在其餘函數中的函數纔可能須要處理不在全局做用域中的外部變量。

5、nonlocal聲明

在上面的例子你仔細看可能會以爲彆扭,不舒服,由於本來是想計算平均值,而又要用一個series記錄下列表,而後在列表中記錄下全部的值,而且計算總和在求平均值,這樣作也能夠,只是不符合習慣,而且不高,要知道申請列表空間可比整型要浪費多了,正確的思路應該是作累加,並記錄下數的個數,最後求平均值。

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        count += 1
        total += new_value
        return total / count
    return averager

可是會出現這樣的結果

>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>

這可能跟上面的局部變量問題有些類似,甚至錯誤類型都是相同的,UnboundLocalError: local variable 'count' referenced before assignment,當 count 是數字或任何不可變類型時,count += 1 語句的做用其實與 count = count + 1 同樣。所以,咱們在 averager 的定義體中爲 count 賦值了,這會把 count 變成局部變量。total 變量也受這個問題影響。
上面的series中沒遇到這個問題,由於咱們沒有給 series 賦值,咱們只是調用 series.append,並把它傳給 sum 和 len。也就是說,咱們利用了列表是可變的對象這一事實。

因此對不可變類型來講,進行+=或者*=操做的時候,實際上是又建立了一個相同的類型並賦值,而不是像列表那樣進行追加,這一點也能夠經過id()查看。而在上面的例子中,counttotal都做爲不可變類型而在averager函數內部的局部變量,而不是自由變量。爲了解決這一問題,Python3引入nonlocal聲明。
它的做用是把變量標記爲自由變量,即便在函數中爲變量賦予新值了,也會變成自由變量。若是爲 nonlocal 聲明的變量賦予新值,閉包中保存的綁定會更新。所以上面問題的解決方案就成了

def make_averager():
    count = 0
    total = 0
    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    return averager

6、簡單的裝飾器

在早些時候 (Python Version < 2.4,2004年之前),爲一個函數添加額外功能的寫法是這樣的。

def debug(func):
    def wrapper():
        print "[DEBUG]: enter {}()".format(func.__name__)
        return func()
    return wrapper

def say_hello():
    print "hello!"

say_hello = debug(say_hello)  # 添加功能並保持原函數名不變

上面的debug函數其實已是一個裝飾器了,它對原函數作了包裝並返回了另一個函數,額外添加了一些功能。由於這樣寫實在不太優雅,在後面版本的Python中支持了@語法糖,下面代碼等同於早期的寫法。

def debug(func):
    def wrapper():
        print "[DEBUG]: enter {}()".format(func.__name__)
        return func()
    return wrapper

@debug
def say_hello():
    print "hello!"

這是最簡單的裝飾器,可是有一個問題,若是被裝飾的函數須要傳入參數,那麼這個裝飾器就壞了。由於返回的函數並不能接受參數,你能夠指定裝飾器函數wrapper接受和原函數同樣的參數,好比:

def debug(func):
    def wrapper(something):  # 指定一毛同樣的參數
        print "[DEBUG]: enter {}()".format(func.__name__)
        return func(something)
    return wrapper  # 返回包裝過函數

@debug
def say(something):
    print "hello {}!".format(something)

這樣你就解決了一個問題,但又多了N個問題。由於函數有千千萬,你只管你本身的函數,別人的函數參數是什麼樣子,鬼知道?還好Python提供了可變參數*args和關鍵字參數**kwargs,有了這兩個參數,裝飾器就能夠用於任意目標函數了。

def debug(func):
    def wrapper(*args, **kwargs):  # 指定宇宙無敵參數
        print "[DEBUG]: enter {}()".format(func.__name__)
        print 'Prepare and say...',
        return func(*args, **kwargs)
    return wrapper  # 返回

@debug
def say(something):
    print "hello {}!".format(something)
相關文章
相關標籤/搜索