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中的函数也是对象,因此:
- python中的函数可以被赋值给另一个变量。
- 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