python3 第二十二章,python3
前面我們說了,在python中,一切皆對象。函數也是一個對象,而且函數對象可以被賦值給變數,通過變數也能調用該函數。如:
def sayHello(name): print(name + ' hello')fn = sayHellofn('roy')
以上代碼,輸出:
roy hello
函數對象有一個__name__屬性,可以拿到函數的名字:
def sayHello(name): print(name + ' hello')f =sayHelloprint(f.__name__)print(sayHello.__name__)
以上代碼,輸出:
sayHellosayHello
你會發現,上例中的變數 f 也獲得了sayHello函數的功能,而且本質上它就是 sayHello 函數
如果,我想sayHello()這個函數在調用前先列印一句話,你可能會立刻想到,在函數的實現裡再加一個print。這當然能達到效果,但是這也修改了函數的定義。
事實上,很多時候我們希望在不修改函數定義的情況下增強函數的功能,這種在代碼運行期間動態增加功能的方式,稱之為“裝飾器”(Decorator)。
裝飾器其實就是一個以函數作為參數並返回一個替換函數的可執行函數。讓我們先從簡單的開始:
def log(fn): def inner(): print('調用 fn 之前') fn() return innerdef sayHello(): print('say hello')decorated = log(sayHello)decorated()
以上代碼,輸出:
調用 fn 之前say hello
我們可以說變數 decorated 是 sayHello 的裝飾版——即 sayHello加上一些東西。事實上,如果寫了一個實用的裝飾器,可能會想用裝飾版來代替 sayHello,這樣就總能得到“附帶其他東西”的 sayHello版本。用不著學習任何新的文法,通過將包含函數的變數重新賦值就能輕鬆做到這一點:
def log(fn): def inner(): print('調用 fn 之前') fn() return innerdef sayHello(): print('say hello')sayHello = log(sayHello)sayHello()
以上代碼,輸出:
調用 fn 之前say hello
現在任意調用 sayHello() 都不會得到原來的 sayHello,而是新的裝飾器版!明白了嗎?
現在這個sayHello函數是不帶參數的,那假如帶有參數的,又該怎麼寫呢?看執行個體:
def log(fn): def inner(name): print('調用 fn 之前') fn(name) return innerdef sayHello(name): print(name, 'say hello')sayHello = log(sayHello)sayHello('roy')
以上代碼,輸出:
調用 fn 之前roy say hello
在上面程式碼範例中,用了一個封裝的函數來替換包含函數的變數來實現了裝飾函數:
sayHello = log(sayHello)sayHello('roy')
在Python3中通過在函數定義前添加一個裝飾器名和 @ 符號,來實現對函數的封裝,如:
def log(fn): def inner(name): print('調用 fn 之前') fn(name) return inner@logdef sayHello(name): print(name, 'say hello')sayHello('roy')
以上代碼,輸出:
調用 fn 之前roy say hello
值得注意的是,這種方式和簡單的使用 log 函數的傳回值來替換原始變數的做法沒有什麼不同—— Python 只是添加了一些文法糖來使之看起來更加明確。
上面我們寫了一個裝飾器,但它是硬式編碼,只適用於特定類型的函數——帶有1個參數的函數。內建函式 inner 接收1個參數,然後繼續將參數傳給閉包中的函數。如果我們想要一個能適用任何函數的裝飾器呢?這意味著這個裝飾器必須接收它所裝飾的任何函數的調用資訊,並且在調用這些函數時將傳遞給該裝飾器的任何參數都傳遞給它們,所幸python提供這樣的文法,我們來寫一個通用的裝飾器:
def log(fn): """ 通用的裝飾器 :param fn: :return: """ def inner(*args, **kwargs): print('調用 %s 之前' % fn.__name__) fn(*args, **kwargs) return inner@logdef sayHello(name): print(name, 'say hello')@logdef sayGoodbye(): print('say goodbye')@logdef sayMessage(name, message): print(name, 'say', message)sayHello('roy')print()sayGoodbye()print()sayMessage('roy', 'Hello World')
以上代碼,輸出:
調用 sayHello 之前roy say hello調用 sayGoodbye 之前say goodbye調用 sayMessage 之前roy say Hello World
註:當定義一個函數時,*args 可以表示在調用函數時從迭代器中取出位置參數, 也可以表示在定義函數時接收額外的位置參數。使用 **kwargs 來表示所有未捕獲的關鍵字參數將會被儲存在字典 kwargs 中。此前 args 和 kwargs 都不是 Python 中文法的一部分,但在函數定義時使用這兩個變數名是一種慣例。和 * 的使用一樣,可以在函數調用和定義時使用 **
但,還有個問題,由於log函數是一個Decorator(裝飾器),返回一個函數,所以,原來的 sayGoodbye 函數仍然存在,只是現在同名的sayGoodbye變數指向了新的函數,於是調用sayGoodbye()將執行新函數,即在log函數中返回的inner函數。但你去看經過log函數裝飾之後的函數,它們的__name__已經從原來的'sayGoodbye'變成了'inner'了:
def log(fn): """ 通用的裝飾器 :param fn: :return: """ def inner(*args, **kwargs): print('調用 %s 之前' % fn.__name__) fn(*args, **kwargs) return inner@logdef sayGoodbye(): print('say goodbye')print(sayGoodbye.__name__)
以上代碼,輸出:
inner
因為log函數中返回的那個函數名字就是'inner',所以,需要把原始函數的__name__等屬性複製到inner函數中,否則,有些依賴函數簽名的代碼執行就會出錯。
不需要編寫inner.__name__ = fn.__name__這樣的代碼,Python內建的functools.wraps就是幹這個事的,所以,一個完整的decorator的寫法如下:
import functools
def log(fn): """ 通用的裝飾器 :param fn: :return: """ @functools.wraps(fn) def inner(*args, **kwargs): print('調用 %s 之前' % fn.__name__) fn(*args, **kwargs) return inner@logdef sayGoodbye(): print('say goodbye')print(sayGoodbye.__name__)
以上代碼,輸出:
sayGoodbye
如果Decorator(裝飾器)本身需要傳入參數,那要怎麼做呢?那就需要編寫一個返回decorator的高階函數,寫出來會更複雜。比如,要自訂log的文本:
import functoolsdef log(msg): def decorator(fn): """ 通用的裝飾器 :param fn: :return: """ @functools.wraps(fn) def inner(*args, **kwargs): print('調用 %s 之前' % fn.__name__) print('msg:',msg) fn(*args, **kwargs) return inner return decorator@log('這是一個自訂訊息')def sayGoodbye(): print('say goodbye')sayGoodbye()
以上代碼,輸出:
調用 sayGoodbye 之前msg: 這是一個自訂訊息say goodbye