Python 中的装饰器是一个可调用的函数,它将另一个函数作为参数,并在不明确修改函数的情况下为该函数增加额外的行为。装饰器有能力在每次调用它所包装的函数之前和之后运行额外的代码。这意味着装饰器可以访问和修改输入参数和返回值。装饰器的优点是,它将改变该函数的行为,而不会永久地修改它。在本教程中,我们将看到如何创建我们自己的装饰器,以及检查装饰器在流行的Python包中的使用情况。
Python 中的函数是对象
理解Python中装饰器的第一步是理解一个函数是一个对象。就像字符串、浮点数、int等等是Python中的一个对象一样,函数也是如此。让我们用Python的type()函数来证明这一点。
some_str = 'some string'
a_bool = True
a_float = 1.0
a_int = 1
a_list = [1, 2, 3]
a_dict = {'a': 1, 'b': 2}
a_tuple = (1, 2, 3)
a_set = {1, 2, 3}
print(f'some_str: {type(some_str)}')
print(f'a_bool: {type(a_bool)}')
print(f'a_float: {type(a_float)}')
print(f'a_int: {type(a_int)}')
print(f'a_list: {type(a_list)}')
print(f'a_dict: {type(a_dict)}')
print(f'a_tuple: {type(a_tuple)}')
print(f'a_set: {type(a_set)}')
some_str: <class 'str'>
a_bool: <class 'bool'>
a_float: <class 'float'>
a_int: <class 'int'>
a_list: <class 'list'>
a_dict: <class 'dict'>
a_tuple: <class 'tuple'>
a_set: <class 'set'>
你可以看到,每一个都在其类型输出中带有一个类的关键字。换句话说,它们都是对象。现在来看看这个。
def my_func():
print('my_func')
print(f'my_func: {type(my_func)}')
my_func: <class 'function'>
用 type() 检查这个函数,发现它和前面所有的例子都有相同的类签名。换句话说,**一个函数也是一个对象!**那么我们在 Python 中对对象做什么呢?我们对它们做的一件事是在各种函数和方法之间传递它们,或者把它们分配给其他对象。正是这种灵活性使装饰器在 Python 中成为可能。
函数中的函数
在 Python 中,在另一个函数中定义一个函数是完全合法的。在这个例子中,我们简单地定义了一个内部函数,然后在外部函数被调用时返回它。
def my_func():
def inner_func():
pass
return inner_func
result = my_func()
print(result)
<function my_func.<locals>
你也可以简单地在其他地方定义的另一个函数中调用一个函数。
def random_func():
print('Random stuff')
def my_func():
random_func()
my_func()
Random stuff
你也可以将一个函数赋值给其他的变量,然后通过用**()**字符来调用这个新的变量,使其像一个函数本身一样。
def my_func():
print('Python is eating the world')
some_other_variable = my_func
some_other_variable()
Python is eating the world
如何在 Python 中制作一个装饰器?
我们现在看到了Python中的函数是多么的灵活,它们可以被传递给其他函数,也可以从其他函数中返回,在其他函数中定义,在其他函数中调用,以及分配给变量,还有其他的可能性。这让我们看一下 Python 中装饰器函数的一般语法。

如果你想在一个函数上使用装饰器,你首先必须写一个装饰器函数。大多数装饰器函数都遵循一个类似的大纲。你首先定义一个接受一个函数作为参数的函数。这个函数的名称将是你的装饰器的名称。这可以在下面第1行看到。在装饰器的内部,应该定义一个wrapper()函数。我们在下面第4行看到了这一点。在这个例子中,我们没有做任何会修改原始函数的事情。这是有目的的。我们只是想在这里看到一个典型的装饰器函数的骨架轮廓。最后,wrapper()函数应该返回一些东西,最后,我们返回wrapper函数本身。
def my_decorator(func):
'''Decorator Function'''
def wrapper():
'''Wrapper Function'''
result = func()
return result
return wrapper
要装饰的函数
为了使用装饰器函数,你需要一个函数来装饰。好了,我们来看看这个要装饰的函数,就在这里。它的名字是to_be_decorated()。这个函数唯一做的事情就是返回字符串 "输出装饰"。在这下面,我们打印出函数本身,调用该函数并将返回值放入结果变量。最后,我们打印出结果。
def to_be_decorated():
return 'output to decorate'
print(to_be_decorated)
result = to_be_decorated()
print(result)
<function
使用@装饰那个函数
Python 有一个很好的语法,可以将装饰器应用于一个函数。你所需要做的就是把装饰函数的名字,在它前面加上一个'@'符号,然后把它放在要装饰的函数定义的正上方一行。在下面的代码中,函数to_be_decorated()现在被@my_decorator函数装饰了。
def my_decorator(func):
'''Decorator Function'''
def wrapper():
'''Wrapper Function'''
result = func()
return result
return wrapper
@my_decorator
def to_be_decorated():
return 'output to decorate'
print(to_be_decorated)
result = to_be_decorated()
print(result)
到此为止,我们有意让装饰器函数处于一种状态,实际上并没有修改它所装饰的函数的效果。然而,如果我们运行上面这段代码,就会发生一些有趣的事情。让我们看看有什么变化。
<function my_decorator.<locals>.wrapper at 0x00000211D8096430>
output to decorate
你注意到有什么变化了吗?在向*to_be_decorated()*函数添加装饰器之前,如果我们简单地打印出该函数,我们会看到。
<function to_be_decorated at 0x000001DB267E6310>
在应用了装饰器之后,打印出*to_be_decorated()*现在显示。
<function my_decorator.<locals>.wrapper at 0x00000211D8096430>
引擎盖下
这是怎么发生的呢?@my_decorator语法是这种明确代码的速记。
def to_be_decorated():
return 'output to decorate'
to_be_decorated = my_decorator(to_be_decorated)
用装饰器修改行为
我们现在看到管道是如何与装饰器一起工作的。我们目前的装饰器没有修改它所装饰的函数的任何行为。现在让我们来改变它。下面是**my_decorator()**函数的一个更新版本。
def my_decorator(func):
'''Decorator Function'''
def wrapper():
'''Wrapper Function'''
result = func()
return result.title().replace(' ', ' !##! ')
return wrapper
现在我们把这个装饰器应用到我们的函数上,注意输出是如何改变的
@my_decorator
def to_be_decorated():
return 'output to decorate'
result = to_be_decorated()
print(result)
Output !##! To !##! Decorate
我们也可以将我们的装饰器应用于其他函数。
@my_decorator
def different_func():
return 'A DIFFERENT FUNCTION'
result = different_func()
print(result)
A !##! Different !##! Function
那么,为什么我们要在函数中使用装饰器呢?我的意思是,如果我想改变一个函数,为什么我不直接去编辑这个函数?好吧,考虑一下你正在做一个大型软件项目。假设你想对所有的函数做同样的操作,比如在函数中添加日志。现在,这是个大项目,所以可能有50个不同的函数。我们可以继续编辑每个函数。换句话说,进入每个函数,围绕记录该函数粘贴一些代码,然后继续到下一个函数。另外,我们也可以使用装饰器。装饰器的优点是,它将改变该函数的行为,而不会永久地修改它。所以,假设后来我们决定不再记录这些函数。简单地删除装饰器比进入每个函数并删除几行代码更容易。
带参数的 Python 装饰器
在这一节中,我们来看看如何使用带参数的装饰器。首先,让我们创建一个新的函数。它是一个列表打印机。这个函数接收一个列表作为参数,然后将这个列表转换成适合打印的字符串格式。
def list_printer(lst):
result = '\n'.join(lst)
return result
lst = ['Harry', 'Bob', 'Alice']
result = list_printer(lst)
print(result)
Harry
Bob
Alice
现在让我们添加一个名为**li_decorator()的新装饰器,并将其应用于list_printer()**函数,然后试着运行代码。
def li_decorator(func):
'''Decorator Function'''
def wrapper():
'''Wrapper Function'''
result = func()
return result
return wrapper
@li_decorator
def list_printer(lst):
result = '\n'.join(lst)
return result
lst = ['Harry', 'Bob', 'Alice']
result = list_printer(lst)
print(result)
Traceback (most recent call last):
File "C:\python\decorator.py", line 20, in <module>
result = list_printer(lst)
TypeError: wrapper() takes 0 positional arguments but 1 was given
好的,看起来效果不是很好。原因是,就像现在这样,装饰器函数不支持参数。我们可以通过在装饰器中加入lst参数来解决这个问题,就像这样。
def li_decorator(func):
'''Decorator Function'''
def wrapper(lst):
'''Wrapper Function'''
result = func(lst)
return result
return wrapper
@li_decorator
def list_printer(lst):
result = '\n'.join(lst)
return result
lst = ['Harry', 'Bob', 'Alice']
result = list_printer(lst)
print(result)
Harry
Bob
Alice
*args和**kwargs
上面的解决方案可行,但它是最好的方法吗?事实证明,这可能不是最好的方法。我们希望我们的装饰器是灵活的,这样它们就能与大量的要装饰的函数一起工作。如果只是把单个列表参数硬编码到装饰器中,那么装饰器在具有不同签名的函数上就会失败。Python 通过***args和**kwargs**关键字为这个问题提供了一个很好的解决方案。通过在装饰器函数中使用这两个关键字,该函数可以使用任何数量的位置参数、关键字参数,或者两者的组合。下面是使用*args和**kwargs的更新代码。
def li_decorator(func):
'''Decorator Function'''
def wrapper(*args, **kwargs):
'''Wrapper Function'''
result = func(*args, **kwargs)
return result
return wrapper
@li_decorator
def list_printer(lst):
result = '\n'.join(lst)
return result
lst = ['Harry', 'Bob', 'Alice']
result = list_printer(lst)
print(result)
Harry
Bob
Alice
现在我们将更新装饰器函数,使其将列表转换为HTML无序列表。这应该允许用户传递一个任意长度的字符串列表,而该函数将正确地把内容包装成一个HTML无序列表。下面是该功能的一个快速演绎。
def li_decorator(func):
'''Decorator Function'''
def wrapper(*args, **kwargs):
'''Wrapper Function'''
result = func(*args, **kwargs).split('\n')
for i in range(len(result)):
result[i] = f'<li>{result[i]}</li>'
result = '<ul>\n' + '\n'.join(result) + '\n</ul>'
return result
return wrapper
@li_decorator
def list_printer(lst):
result = '\n'.join(lst)
return result
lst = ['Harry', 'Bob', 'Alice']
<ul>
<li>Harry</li>
<li>Bob</li>
<li>Alice</li>
</ul>
用不同长度的列表调用该函数也能很好地工作。让我们再试一次,在没有装饰器的情况下,以及在应用装饰器的情况下,使用一个较长的名字列表。
没有装饰器
def list_printer(lst):
result = '\n'.join(lst)
return result
lst = ['Susan', 'Christopher', 'John', 'David', 'William']
result = list_printer(lst)
print(result)
Susan
Christopher
John
David
William
使用装饰器
@li_decorator
def list_printer(lst):
result = '\n'.join(lst)
return result
lst = ['Susan', 'Christopher', 'John', 'David', 'William']
result = list_printer(lst)
print(result)
<ul>
<li>Susan</li>
<li>Christopher</li>
<li>John</li>
<li>David</li>
<li>William</li>
</ul>
函数包
通过用另一个可调用的函数替换一个函数,会有一些元数据的损失。这可能会使调试更加棘手。让我们在一个例子中看看我们这样做的意思。考虑这个没有装饰的函数,我们打印出函数的元数据名称、元数据文档和帮助属性。
def list_printer(lst):
'''Convert list to string'''
result = '\n'.join(lst)
return result
print(list_printer.__name__)
print(list_printer.__doc__)
help(list_printer)
list_printer
Convert list to string
Help on function list_printer in module __main__:
list_printer(lst)
Convert list to string
你已经有了list_printer这个名字,我们已经有了list_printer的docstring。现在许多编辑器和帮助函数都使用docstring。因此,举例来说,如果我们输入help和list_printer,我们就会得到list_printer函数的文档串。当这个同样的函数被装饰时,会发生什么?让我们来看看。
@li_decorator
def list_printer(lst):
'''Convert list to string'''
result = '\n'.join(lst)
return result
print(list_printer.__name__)
print(list_printer.__doc__)
help(list_printer)
wrapper
Wrapper Function
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
Wrapper Function
现在我们看到一个非常不同的结果。这一次我们得到了包装函数的细节。查看 list_printer dunder doc,我们得到了包装函数的 docstring。现在,这是因为装饰器函数 li_maker 正在返回包装器。这是一个意外的结果,因为我们想要 list_printer 函数的名称和 docstring。Python 通过 functools 包的 wraps() 模块提供了一个简单的解决方案。
from functools import wraps
def li_decorator(func):
'''Decorator Function'''
@wraps(func)
def wrapper(*args, **kwargs):
'''Wrapper Function'''
result = func(*args, **kwargs).split('\n')
for i in range(len(result)):
result[i] = f'<li>{result[i]}</li>'
result = '<ul>\n' + '\n'.join(result) + '\n</ul>'
return result
return wrapper
@li_decorator
def list_printer(lst):
'''Convert list to string'''
result = '\n'.join(lst)
return result
print(list_printer.__name__)
print(list_printer.__doc__)
help(list_printer)
list_printer
Convert list to string
Help on function list_printer in module __main__:
list_printer(lst)
Convert list to string
你可以看到,通过使用来自 functools 的 wraps,被装饰的函数的元数据不再丢失。这在调试你的代码时可能会有帮助。
Python 类装饰器
让我们看一个如何在Python中使用装饰器的例子。下面的例子 LiDecorator 类提供了与我们在上面的函数装饰器中看到的相同的功能。它把 list_printer() 函数变成一个 HTML 无序列表打印机。让我们研究一下函数装饰器和类装饰器之间的一些区别。
- 首先,我们可以看到,我们使用了update_wrapper()函数,而不是functools模块中的@wraps()。
- 接下来,我们看到,类本身的名字就是装饰器的名字。所以在这里的例子中,LiDecorator是类的名字,因此我们在调用装饰器的时候使用**@LiDecorator**。
- 在**__init__方法中,我们当然接受自我对象,但也接受func**可调用的参数。
- __call__方法等同于基于函数的装饰器中的 wrapper() 函数。
from functools import update_wrapper
class LiDecorator:
def __init__(self, func):
update_wrapper(self, func)
self.func = func
def __call__(self, *args, **kwargs):
'''Wrapper Function'''
result = self.func(*args, **kwargs).split('\n')
for i in range(len(result)):
result[i] = f'<li>{result[i]}</li>'
result = '<ul>\n' + '\n'.join(result) + '\n</ul>'
return result
@LiDecorator
def list_printer(lst):
'''Convert list to string'''
result = '\n'.join(lst)
return result
result = list_printer(['Lisa', 'Bart', 'Maggie'])
print(result)
<ul>
<li>Lisa</li>
<li>Bart</li>
<li>Maggie</li>
</ul>
使用装饰器的流行 Python 库
我们现在对 Python 中的装饰器是如何构造的,以及它们的用途有了相当好的了解。所以你可能想知道,Python装饰器有什么用?装饰器非常流行,在著名的 Python 项目中被广泛使用。最常引用的例子是Flask和Django。例如,你在Flask中使用装饰器定义路由。在Django中,你有视图装饰器,比如@require_http_methods(["GET", "POST"]),它决定了允许在视图函数中使用的HTTP动词。Django还提供了一个非常有用的login_required()装饰器,它可以通过简单地将一行装饰器应用到相关的视图中来保护任何页面不被未经认证的用户访问。这些都是很好的例子,说明什么是装饰器的用途。
什么是Python中的装饰器摘要
现在我们了解了什么是装饰器以及它们在Python中是如何工作的,现在是对它们进行实验的好时机。在标准 Python 库中有几个内置函数使用了装饰器。这些函数包括@propertydecorator,@staticmethoddecorator, 以及@classmethoddecorator。看一看这些装饰器,看看你是否能从文档中了解到如何使用它们。另一个学习装饰器的好地方是Flask框架,因为它广泛地使用了装饰器。
