Decorator基本指南
前提知識
Python中的閉包(closure)
所謂閉包,指的是附帶數(shù)據(jù)的函數(shù)對象。關(guān)于閉包的詳解,請參閱我的另一篇文章
Python中函數(shù)也是對象
要想理解裝飾器,我們首先必須明確一個概念,那就是Python中的函數(shù)(function)也是對象,可以被作為參數(shù)傳遞,可以被賦值給另一個變量,等等。如下例子說明了這一點。
def shout(word='yes'):
return word.capitalize() + '!'
print(shout())
# output: 'YES!'
# 函數(shù)作為一個對象,你也可以將其賦值給另一個變量,就像任何其他我們熟知的對象一樣
scream = shout
print(scream())
# output: 'YES!'
# 除此之外,即使原本的函數(shù)變量`shout`被刪除了,通過賦值得到的新變量`scream`還是能夠被用來正常調(diào)用該函數(shù)
del shout
print(shout())
# NameError: name 'shout' is not defined
print(scream())
# output: 'YES!'
Python中一個函數(shù)可以被定義在另一個函數(shù)內(nèi)部
同時,在python中,我們也可以在一個函數(shù)內(nèi)部定義另一個函數(shù)。如下面例子所示。
def talk():
# 在函數(shù)中定義另一個函數(shù)
def whisper(word='yes'):
return word.lower() + '...'
# 然后我們可以在函數(shù)中調(diào)用這個新定義的函數(shù)
print whisper()
# 然后我們可以調(diào)用`talk`函數(shù),該函數(shù)每次都動態(tài)地定義一個`whisper`函數(shù),接著`talk`函數(shù)又調(diào)用了新定義的`whisper`函數(shù)
talk()
# output: 'yes...'
# 但是在`talk`函數(shù)之外并不存在一個`whisper`函數(shù)
whisper()
# NameError: name 'whisper' is not defined
那么,根據(jù)以上兩個小結(jié)的知識,我們知道python中的函數(shù)也是對象,因此:
- python中的函數(shù)可以被賦值給另一個變量。
- python中的函數(shù)可以非常靈活地在各種位置被定義,包括另一個函數(shù)內(nèi)部。
因而我們甚至可以把一個函數(shù)作為另一個函數(shù)的返回值。如下面的例子所示。
def get_talk(return_type='shout'):
# 我們在這一函數(shù)中動態(tài)定義另外一些函數(shù)
def shout(word='yes'):
return word.capitalize() + '!'
def whisper(word='yes'):
return word.lower() + '...'
# 然后我們基于`type`的值返回其中一個函數(shù)
# 注意以下兩個返回值中我們沒有包含函數(shù)后的括號,因為我們返回的是函數(shù)本身而不是調(diào)用函數(shù)的結(jié)果
if return_type == 'shout':
return shout
else:
return whisper
那么對于這樣一個返回函數(shù)的函數(shù),我們應該如何調(diào)用呢?參考前文中的內(nèi)容,在python函數(shù)也是對象,我們可以講函數(shù)賦值給一個變量。因此我們只需要將上面定義的函數(shù)的返回值賦值給其他變量再調(diào)用即可。如下文中所示。
talk = get_talk()
# 此處的`talk`是一個函數(shù)對象
print(talk)
# output: <function shout at 0xc7ae472c>
# 我們可以調(diào)用這個函數(shù)
print(talk())
# output Yes!
# 與此類似地,我們可以也可以直接調(diào)用這個返回的函數(shù)而不將其賦值給另一個變量
print(getTalk('whisper')())
# output: yes...
同樣地,既然我們可以將一個函數(shù)作為另一個函數(shù)的返回值,那么我們也可以將一個函數(shù)作為另一個函數(shù)的輸入?yún)?shù)。
def function_as_argument(func):
print('this function accept another function as input argument')
print(func())
function_as_argument(shout())
# output:
# this function accept another function as input argument
# Yes!
到此為止,讀者們已經(jīng)掌握了理解python裝飾器所需要的全部知識。
什么是裝飾器
本質(zhì)上來說,python中的裝飾器其實只是對其所裝飾的函數(shù)的一層額外包裝。其實現(xiàn)方法與我們上文所討論的代碼邏輯類似,即接受一個函數(shù)作為輸入,然后定義另外一個包裝函數(shù)在其執(zhí)行前后加入另外一些邏輯,最終返回這個包裝函數(shù)。在裝飾器中,我們可以完全不修改原有函數(shù)的情況下,執(zhí)行所裝飾的函數(shù)之外另外包裝一些別的代碼邏輯。
實現(xiàn)一個基本的裝飾器
基本的裝飾器邏輯的實現(xiàn)如下面的代碼所示。
其基本邏輯為:在一個裝飾器函數(shù)中,我們首先定義另外一個包裝函數(shù),這個函數(shù)將負責在我們所要裝飾的函數(shù)前后文中添加我們需要的代碼邏輯(也就是將需要被裝飾的函數(shù)包裝起來)。然后在裝飾器函數(shù)中,我們將這一包裝函數(shù)作為返回值返回。
# 裝飾器就是接受一個函數(shù)作為輸入,并返回另一個函數(shù)的函數(shù)
def basic_decorator_logic(func_to_decorate):
# 定義包裝函數(shù)
def the_wrapper_around_the_original_function():
# 在這里添加需要在被裝飾的原始函數(shù)執(zhí)行之前執(zhí)行的邏輯
print('Before the original function runs')
# 調(diào)用原始函數(shù)
# 這里體現(xiàn)了python中閉包的概念
func_to_decorate()
# 在這里添加需要在被裝飾的原始函數(shù)執(zhí)行之后執(zhí)行的邏輯
print('After the original function runs')
# 然后我們返回這一在當前裝飾器函數(shù)中動態(tài)定義的包裝函數(shù)
# 這一動態(tài)定義的包裝函數(shù)`the_wrapper_around_the_original_function`包含需要在被裝飾函數(shù)執(zhí)行前后需要添加的邏輯以及被包裝函數(shù)的執(zhí)行
# 注意這里返回的是動態(tài)定義的包裝函數(shù)對象本身,而不是包裝函數(shù)的執(zhí)行結(jié)果
return the_wrapper_around_the_original_function
到這里,我們已經(jīng)親手實現(xiàn)了一個簡單的裝飾器函數(shù)。下面的示例代碼將說明如何使用這一裝飾器函數(shù)。
def function_we_want_to_decorate():
print('This is a function that is going to be decorated, we can add additional execution logic without changing the function')
functin_we_want_to_decorate()
# output: This is a function that is going to be decorated, we can add additional execution logic without changing the function
# 我們只需要將`function_we_want_to_decorate`作為參數(shù)傳入我們上面定義的裝飾器函數(shù)中,就可以獲得一個被包裝過的新函數(shù)。
# 這一新函數(shù)中包含了一些我們額外添加的邏輯
decorated_function = basic_decorator_logic(function_we_want_to_decorate)
decorated_function()
# output:
# Before the original function runs
# This is a function that is going to be decorated, we can add additional execution logic without changing the function
# After the original function runs
考慮到python中使用裝飾器往往是為了在后文中完全用裝飾過后的函數(shù)替代我們原本定義的函數(shù),我們可以將裝飾過后的函數(shù)賦值給原函數(shù)對應的變量名,從而在代碼下文中實現(xiàn)永久替換,如下面的例子所示。
functin_we_want_to_decorate()
# output: This is a function that is going to be decorated, we can add additional execution logic without changing the function
function_we_want_to_decoratre = basic_decorator_logic(function_we_want_to_decorate)
function_we_want_to_decorate()
# output:
# Before the original function runs
# This is a function that is going to be decorated, we can add additional execution logic without changing the function
# After the original function runs
讀到這里,相信讀者們已經(jīng)發(fā)現(xiàn),我們上面這一段代碼的邏輯與表現(xiàn)和python中以@打頭標注的裝飾器完全相同。事實上這就是裝飾器背后的邏輯。
用@標識的python裝飾器
事實上,我們完全可以用python的裝飾器語法來重寫上面的示例代碼。如下所示。
@basic_decorator_logic
def function_we_want_to_decorate():
print('This is a function that is going to be decorated, we can add additional execution logic without changing the function')
function_we_want_to_decorate()
# output:
# Before the original function runs
# This is a function that is going to be decorated, we can add additional execution logic without changing the function
# After the original function runs
事實上,python中的@語法是一種縮寫,如下所示:
@decorator
def func():
pass
等同于
func = decorator(func)
進一步地,我們也可以對一個函數(shù)使用多個裝飾器。根據(jù)上述邏輯,相信聰明的讀者們也就明白了多層裝飾器的執(zhí)行順序。只要根據(jù)縮寫將裝飾器展開,我們自然就發(fā)現(xiàn)多層裝飾器將被從里到外執(zhí)行,也就是對于同一個函數(shù)定義上方的裝飾器,最上面一行的裝飾器將被最后套用,而最下面一行的裝飾器將被最先套用。如下面例子所示。
def outer_decorator(func):
def wrapper():
print('outer_wrapper_before')
func()
print('outer_wrapper_aftrer')
def inner_decorator(func):
def wrapper():
print('inner_wrapper_before')
func()
print('inner_wrapper_after')
def hotpot(sentence='I love hotpot!'):
print(sentence)
func()
# output: I love hotpot!
func = outer_decorator(inner_decorator(func))
func()
# output:
# outer_wrapper_before
# inner_wrapper_before
# I love hotpot!
# inner_wrapper_after
# outer_wrapper_after
@outer_decorator
@inner_decorator
def beijing_duck(sentence='I love beijing duck!'):
print(sentence)
beijing_duck()
# output:
# outer_wrapper_before
# inner_wrapper_before
# I love beijing duck!
# inner_wrapper_after
# outer_wrapper_after
# 下面的例子進一步說明了裝飾器被執(zhí)行的順序
@inner_decorator
@outer_decorator
def gopnik(sentence='Gopnik!'):
print('adidas, adidas hard bass and ignoring gravity are the 3 most important factors for gopnik dance! Check it out on bilibili and you will laugh your ass off.')
gopnik()
# output:
# inner_wrapper_before
# outer_wrapper_before
# adidas, adidas hard bass and ignoring gravity are the 3 most important factors for gopnik dance! Check it out on bilibili and you will laugh your ass off.
# outer_wrapper_after
# inner_wrapper_after
裝飾器的一個實際使用示例
例如我們?nèi)绻胍獙憙蓚€裝飾器,一個能夠自動給字符串增加斜體HTML tag,一個能夠自動給字符串增加黑體HTML tag,我們應該如何實現(xiàn)呢?
假定我們已有一個函數(shù)能夠返回字符串,而我們的裝飾器要做的就是讓這個函數(shù)變?yōu)榉祷丶恿诵斌w或黑體tag的字符串的函數(shù)。如下面的代碼所示。
# 增加黑體tag的裝飾器
def make_bold(func):
def wrapper():
return '<b>{}</b>'.format(func())
return wrapper
# 增加斜體tag的裝飾器
def make_italic(func):
def wrapper():
return '<i>{}</i>'.format(func())
return wrapper
@make_bold
@make_italic
def say():
return 'hello'
print(say())
# output: <b><i>hello</i></b>
# 如上文所述,以上代碼等同與下面這段代碼
def say():
return 'hello'
say = make_bold(make_italic(say))
print(say())
# output: <b><i>hello</b></i>
很好,到此為止,相信讀者朋友們能已經(jīng)能夠完全能理解裝飾器背后的邏輯及實現(xiàn)方法,并且已經(jīng)理解了如何自定義一個裝飾器來實現(xiàn)自己需要的功能了!
Reference
文中部分內(nèi)容翻譯自如下文章。翻譯部分版權(quán)歸原作者所有。
https://gist.github.com/Zearin/2f40b7b9cfc51132851a