Python裝飾器(Decorator)完全指南-基礎(chǔ)篇

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ù)也是對象,因此:

  1. python中的函數(shù)可以被賦值給另一個變量。
  2. 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容