Python装饰器(Decorator)完全指南-基础篇

Decorator基本指南

前提知识

Python中的闭包(closure)

所谓闭包,指的是附带数据的函数对象。关于闭包的详解,请参阅我的另一篇文章

Python中函数也是对象

要想理解装饰器,我们首先必须明确一个概念,那就是Python中的函数(function)也是对象,可以被作为参数传递,可以被赋值给另一个变量,等等。如下例子说明了这一点。

def shout(word='yes'):
    return word.capitalize() + '!'

print(shout())
# output: 'YES!'

# 函数作为一个对象,你也可以将其赋值给另一个变量,就像任何其他我们熟知的对象一样
scream = shout
print(scream())
# output: 'YES!'

# 除此之外,即使原本的函数变量`shout`被删除了,通过赋值得到的新变量`scream`还是能够被用来正常调用该函数
del shout
print(shout())
# NameError: name 'shout' is not defined

print(scream())
# output: 'YES!'

Python中一个函数可以被定义在另一个函数内部

同时,在python中,我们也可以在一个函数内部定义另一个函数。如下面例子所示。

def talk():
    # 在函数中定义另一个函数
    def whisper(word='yes'):
        return word.lower() + '...'

    # 然后我们可以在函数中调用这个新定义的函数
    print whisper()

# 然后我们可以调用`talk`函数,该函数每次都动态地定义一个`whisper`函数,接着`talk`函数又调用了新定义的`whisper`函数
talk()
# output: 'yes...'

# 但是在`talk`函数之外并不存在一个`whisper`函数

whisper()
# NameError: name 'whisper' is not defined

那么,根据以上两个小结的知识,我们知道python中的函数也是对象,因此:

  1. python中的函数可以被赋值给另一个变量。
  2. python中的函数可以非常灵活地在各种位置被定义,包括另一个函数内部。

因而我们甚至可以把一个函数作为另一个函数的返回值。如下面的例子所示。

def get_talk(return_type='shout'):
    # 我们在这一函数中动态定义另外一些函数
    def shout(word='yes'):
        return word.capitalize() + '!'

    def whisper(word='yes'):
        return word.lower() + '...'
 
    # 然后我们基于`type`的值返回其中一个函数
    # 注意以下两个返回值中我们没有包含函数后的括号,因为我们返回的是函数本身而不是调用函数的结果
    if return_type == 'shout':
        return shout
    else:
        return whisper

那么对于这样一个返回函数的函数,我们应该如何调用呢?参考前文中的内容,在python函数也是对象,我们可以讲函数赋值给一个变量。因此我们只需要将上面定义的函数的返回值赋值给其他变量再调用即可。如下文中所示。

talk = get_talk()

# 此处的`talk`是一个函数对象
print(talk)
# output: <function shout at 0xc7ae472c>

# 我们可以调用这个函数
print(talk())
# output Yes!

# 与此类似地,我们可以也可以直接调用这个返回的函数而不将其赋值给另一个变量
print(getTalk('whisper')())
# output: yes...

同样地,既然我们可以将一个函数作为另一个函数的返回值,那么我们也可以将一个函数作为另一个函数的输入参数。

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!

到此为止,读者们已经掌握了理解python装饰器所需要的全部知识。

什么是装饰器

本质上来说,python中的装饰器其实只是对其所装饰的函数的一层额外包装。其实现方法与我们上文所讨论的代码逻辑类似,即接受一个函数作为输入,然后定义另外一个包装函数在其执行前后加入另外一些逻辑,最终返回这个包装函数。在装饰器中,我们可以完全不修改原有函数的情况下,执行所装饰的函数之外另外包装一些别的代码逻辑。

实现一个基本的装饰器

基本的装饰器逻辑的实现如下面的代码所示。

其基本逻辑为:在一个装饰器函数中,我们首先定义另外一个包装函数,这个函数将负责在我们所要装饰的函数前后文中添加我们需要的代码逻辑(也就是将需要被装饰的函数包装起来)。然后在装饰器函数中,我们将这一包装函数作为返回值返回。

# 装饰器就是接受一个函数作为输入,并返回另一个函数的函数
def basic_decorator_logic(func_to_decorate): 
    # 定义包装函数
    def the_wrapper_around_the_original_function():
        # 在这里添加需要在被装饰的原始函数执行之前执行的逻辑
        print('Before the original function runs')

        # 调用原始函数
        # 这里体现了python中闭包的概念
        func_to_decorate()

        # 在这里添加需要在被装饰的原始函数执行之后执行的逻辑
        print('After the original function runs')

    # 然后我们返回这一在当前装饰器函数中动态定义的包装函数
    # 这一动态定义的包装函数`the_wrapper_around_the_original_function`包含需要在被装饰函数执行前后需要添加的逻辑以及被包装函数的执行
    # 注意这里返回的是动态定义的包装函数对象本身,而不是包装函数的执行结果
    return the_wrapper_around_the_original_function

到这里,我们已经亲手实现了一个简单的装饰器函数。下面的示例代码将说明如何使用这一装饰器函数。

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`作为参数传入我们上面定义的装饰器函数中,就可以获得一个被包装过的新函数。
# 这一新函数中包含了一些我们额外添加的逻辑
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中使用装饰器往往是为了在后文中完全用装饰过后的函数替代我们原本定义的函数,我们可以将装饰过后的函数赋值给原函数对应的变量名,从而在代码下文中实现永久替换,如下面的例子所示。

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

读到这里,相信读者们已经发现,我们上面这一段代码的逻辑与表现和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)

进一步地,我们也可以对一个函数使用多个装饰器。根据上述逻辑,相信聪明的读者们也就明白了多层装饰器的执行顺序。只要根据缩写将装饰器展开,我们自然就发现多层装饰器将被从里到外执行,也就是对于同一个函数定义上方的装饰器,最上面一行的装饰器将被最后套用,而最下面一行的装饰器将被最先套用。如下面例子所示。

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

# 下面的例子进一步说明了装饰器被执行的顺序
@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

装饰器的一个实际使用示例

例如我们如果想要写两个装饰器,一个能够自动给字符串增加斜体HTML tag,一个能够自动给字符串增加黑体HTML tag,我们应该如何实现呢?

假定我们已有一个函数能够返回字符串,而我们的装饰器要做的就是让这个函数变为返回加了斜体或黑体tag的字符串的函数。如下面的代码所示。

# 增加黑体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>

很好,到此为止,相信读者朋友们能已经能够完全能理解装饰器背后的逻辑及实现方法,并且已经理解了如何自定义一个装饰器来实现自己需要的功能了!

Reference
文中部分内容翻译自如下文章。翻译部分版权归原作者所有。
https://gist.github.com/Zearin/2f40b7b9cfc51132851a

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容