课程
装饰器是 Python 中强大而优雅的特性,能够在不修改函数与方法源代码的情况下,修改或扩展其行为。
装饰器是一种 Python 设计模式,允许在不改变对象结构的前提下为其添加新功能。装饰器通常应用于函数,在增强或修改函数行为方面起着关键作用。按照惯例,装饰器放在要装饰的函数定义之前。在本教程中,我们将演示如何在Python 函数中有效使用装饰器。
要点速览
- 装饰器可在不更改原始代码的情况下修改或扩展函数行为
- 使用
@decorator_name语法可更简洁地应用装饰器 - 编写装饰器时务必使用
functools.wraps以保留函数元数据 - 基于类的装饰器可在多次调用间维护有状态行为
- 常见用例包括缓存(
@lru_cache)、日志、认证与输入校验
函数是一等公民
Python 中的函数是一等公民。这意味着函数可以被作为参数传递、由函数返回、被修改并赋值给变量。这一特性至关重要,因为它让函数可以像 Python 中的其他对象一样被处理,从而带来更大的编程灵活性。
若想轻松运行本教程中的全部示例代码,您可以免费创建一个 DataLab 工作簿,其中预装了 Python 并包含所有代码示例。若需更多装饰器练习,请查看这道 DataCamp 动手练习。
将函数赋值给变量
首先,我们创建一个函数,每次调用时为数字加一。随后将该函数赋值给变量,并通过该变量来调用函数。
def plus_one(number):
return number + 1
add_one = plus_one
add_one(5)6在函数内部定义函数
在函数内部定义函数是 Python 的强大特性——也是构建装饰器的基础。接下来看看另一个核心思想:将函数作为参数传递。这会让我们更接近编写装饰器。
def plus_one(number):
def add_one(number):
return number + 1
result = add_one(number)
return result
plus_one(4)
5
将函数作为参数传递给其他函数
函数也可以作为参数传入其他函数。下面通过示例说明。
def plus_one(number):
return number + 1
def function_call(function):
number_to_add = 5
return function(number_to_add)
function_call(plus_one)
6
函数返回其他函数
函数也可以生成并返回另一个函数。我们用例子展示如下。
def hello_function():
def say_hi():
return "Hi"
return say_hi
hello = hello_function()
hello()
'Hi'
内部函数与闭包
Python 允许嵌套函数访问其外层函数的外部作用域。这在装饰器中至关重要,被称为闭包。
Python 中的闭包,是指一个函数在其创建环境不再活跃后,仍能记住并访问该环境。这意味着嵌套函数可以“捕获”外层作用域的变量并继续使用它们。
理解闭包对于理解装饰器至关重要,因为装饰器依赖内部包装函数能够访问并修改外层装饰器函数中的状态或参数。
闭包示例:
def outer_function(message):
def inner_function():
print(f"Message from closure: {message}")
return inner_function
closure_function = outer_function("Hello, closures!")
closure_function()
# Output: Message from closure: Hello, closures!在该示例中:
inner_function是闭包,因为它访问了外层作用域(outer_function)中的变量message。- 即使
outer_function已执行完毕,inner_function仍保留对message的访问。
当您创建装饰器时,装饰器内部的包装函数就是一个闭包。它会记住被装饰的函数以及装饰器中定义的任何额外状态或参数。例如:
def simple_decorator(func):
def wrapper():
print("Before the function call")
func()
print("After the function call")
return wrapper
@simple_decorator
def greet():
print("Hello!")
greet()
# Output:
# Before the function call
# Hello!
# After the function call这里,wrapper 是一个闭包,它记住了函数 greet,并在其执行前后添加了行为。
创建您的第一个装饰器
既然已经理解闭包——即函数记住其外层作用域变量的能力——我们就可以创建第一个真正的装饰器了。闭包是装饰器在幕后发挥作用的“秘密武器”。
我们来编写一个简单的装饰器,把句子转换成大写。方法是在外层函数中定义一个包装器。可以看到,它与之前在函数内部再定义函数的结构非常相似。
def uppercase_decorator(function):
def wrapper():
func = function()
make_uppercase = func.upper()
return make_uppercase
return wrapper
由于我们的装饰器接收函数作为参数,我们将定义一个新函数并将其传给装饰器。我们先前学到可以把函数赋值给变量。这里就用这个技巧来调用装饰器函数。
def say_hi():
return 'hello there'
decorate = uppercase_decorator(say_hi)
decorate()
'HELLO THERE'
使用 @ 语法
不过,Python 为我们提供了更简便的方式来应用装饰器。只需在要装饰的函数前使用 @ 符号。如下所示:
@uppercase_decorator
def say_hi():
return 'hello there'
say_hi()
'HELLO THERE'
为单个函数叠加多个装饰器
熟悉了单个装饰器的 @ 语法后,您可以更进一步,在同一函数上叠加多个装饰器。请记住:顺序很重要!
下面定义另一个装饰器,将句子拆分为列表。然后把 uppercase_decorator 与 split_string 装饰器同时应用到一个函数上。
import functools
def split_string(function):
@functools.wraps(function)
def wrapper():
func = function()
splitted_string = func.split()
return splitted_string
return wrapper @split_string
@uppercase_decorator
def say_hi():
return 'hello there'
say_hi()
['HELLO', 'THERE']
从以上输出可以看出,装饰器的应用顺序是自下而上。如果交换顺序,会报错,因为列表没有 upper 属性。这里先将句子转换为大写,再拆分成列表。
注意:在叠加装饰器时,常用做法是使用 functools.wraps 来确保在整个叠加过程中保留原始函数的元数据。这有助于在调试和理解被装饰函数属性时保持清晰与一致。
在装饰器中接收参数
目前为止,我们看到的装饰器仅仅是对函数进行包装。但如果您希望对装饰器本身进行配置——例如向其传入参数,该怎么办?这就需要用到装饰器工厂。
def decorator_with_arguments(function):
def wrapper_accepting_arguments(arg1, arg2):
print("My arguments are: {0}, {1}".format(arg1,arg2))
function(arg1, arg2)
return wrapper_accepting_arguments
@decorator_with_arguments
def cities(city_one, city_two):
print("Cities I love are {0} and {1}".format(city_one, city_two))
cities("Nairobi", "Accra")My arguments are: Nairobi, Accra
Cities I love are Nairobi and Accra
注意:必须确保装饰器中的参数(arg1, arg2,如本例)与被包装函数的参数(cities,如本例)数量一致。保持一致对于避免错误并确保带参数装饰器的正常工作至关重要。
使用 *args 和 **kwargs 的通用装饰器
若要定义可用于任意函数的通用装饰器,我们使用 *args 和 **kwargs。*args 与 **kwargs 会收集所有位置参数与关键字参数,并分别存入 *args 与 **kwargs 变量。借助 *args 和 **kwargs,我们可以在函数调用时传入任意数量的参数。
def a_decorator_passing_arbitrary_arguments(function_to_decorate):
def a_wrapper_accepting_arbitrary_arguments(*args,**kwargs):
print('The positional arguments are', args)
print('The keyword arguments are', kwargs)
function_to_decorate(*args)
return a_wrapper_accepting_arbitrary_arguments
@a_decorator_passing_arbitrary_arguments
def function_with_no_argument():
print("No arguments here.")
function_with_no_argument()The positional arguments are ()
The keyword arguments are {}
No arguments here.
下面看看如何用位置参数来使用该装饰器。
@a_decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
print(a, b, c)
function_with_arguments(1,2,3)
The positional arguments are (1, 2, 3)
The keyword arguments are {}
1 2 3
如下所示,您也可以向被装饰的函数传入关键字参数:
@a_decorator_passing_arbitrary_arguments
def function_with_keyword_arguments():
print("This has shown keyword arguments")
function_with_keyword_arguments(first_name="Derrick", last_name="Mwiti")
The positional arguments are ()
The keyword arguments are {'first_name': 'Derrick', 'last_name': 'Mwiti'}
This has shown keyword arguments
注意:在装饰器中使用 **kwargs 可处理关键字参数。这让通用装饰器更通用,能够在函数调用时处理多种参数类型。
向装饰器本身传参
现在看看如何向装饰器本身传递参数。为此,我们定义一个接收参数的装饰器生成器,然后在其中定义装饰器。接着像之前一样,在装饰器内定义包装函数。
def decorator_maker_with_arguments(decorator_arg1, decorator_arg2, decorator_arg3):
def decorator(func):
def wrapper(function_arg1, function_arg2, function_arg3) :
"This is the wrapper function"
print("The wrapper can access all the variables\n"
"\t- from the decorator maker: {0} {1} {2}\n"
"\t- from the function call: {3} {4} {5}\n"
"and pass them to the decorated function"
.format(decorator_arg1, decorator_arg2,decorator_arg3,
function_arg1, function_arg2,function_arg3))
return func(function_arg1, function_arg2,function_arg3)
return wrapper
return decorator
pandas = "Pandas"
@decorator_maker_with_arguments(pandas, "Numpy","Scikit-learn")
def decorated_function_with_arguments(function_arg1, function_arg2,function_arg3):
print("This is the decorated function and it only knows about its arguments: {0}"
" {1}" " {2}".format(function_arg1, function_arg2,function_arg3))
decorated_function_with_arguments(pandas, "Science", "Tools")The wrapper can access all the variables
- from the decorator maker: Pandas Numpy Scikit-learn
- from the function call: Pandas Science Tools
and pass them to the decorated function
This is the decorated function, and it only knows about its arguments: Pandas Science Tools
调试装饰器
如前所述,装饰器会包装函数。原始函数名、文档字符串和参数列表都会被包装器闭包隐藏:例如,当我们尝试访问 decorated_function_with_arguments 的元数据时,看到的将是包装器闭包的元数据。这给调试带来挑战。
decorated_function_with_arguments.__name__
'wrapper'
decorated_function_with_arguments.__doc__
'This is the wrapper function'
为解决这一问题,Python 提供了 functools.wraps装饰器。它会把未装饰函数中丢失的元数据复制回被装饰的闭包。如下所示:
import functools
def uppercase_decorator(func):
@functools.wraps(func)
def wrapper():
return func().upper()
return wrapper
@uppercase_decorator
def say_hi():
"This will say hi"
return 'hello there'
say_hi()
'HELLO THERE'
现在,当我们检查 say_hi 的元数据时,会发现它正确地反映了原始函数,而非包装器。
say_hi.__name__
'say_hi'
say_hi.__doc__
'This will say hi'
建议并且应当始终在定义装饰器时使用 functools.wraps。这将为调试省去大量麻烦。
基于类的装饰器
虽然基于函数的装饰器更常见,Python 也允许创建基于类的装饰器,尤其在复杂场景下能提供更高的灵活性与可维护性。如果您对Python 面向对象编程还不熟悉,先回顾基础会更有帮助。基于类的装饰器是一个带有 __call__ 方法的类,使其实例可像函数一样被调用。
class UppercaseDecorator:
def __init__(self, function):
self.function = function
def __call__(self, *args, **kwargs):
result = self.function(*args, **kwargs)
return result.upper()
@UppercaseDecorator
def greet():
return "hello there"
print(greet())
# Output: HELLO THERE工作原理:
__init__方法用被装饰的函数进行初始化。- 当调用被装饰函数时,会触发
__call__方法,从而允许装饰器修改其行为。
基于类的装饰器优势:
- 有状态装饰器:可用实例变量维护状态;而函数式装饰器往往需要闭包或全局变量。
- 可读性:对于复杂装饰器,将逻辑封装到类中可使代码更有条理、更易理解。
有状态装饰器示例:
class CallCounter:
def __init__(self, function):
self.function = function
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Function {self.function.__name__} has been called {self.count} times.")
return self.function(*args, **kwargs)
@CallCounter
def say_hello():
print("Hello!")
say_hello()
say_hello()
# Output:
# Function say_hello has been called 1 times.
# Hello!
# Function say_hello has been called 2 times.
# Hello!装饰器的真实用例:缓存
lru_cache 是 Python 内置的装饰器,用于缓存开销较大的函数调用结果。对于重复输入,可通过避免重复计算来提升性能。
示例:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # Subsequent calls with the same argument are much faster装饰器的其他常见用途:
日志:跟踪函数调用、参数与返回值,用于调试或审计。
认证:在 Flask 或 Django 等 Web 框架中实施访问控制。
执行计时:测量并优化函数执行时间,适用于对性能敏感的任务。
重试机制:自动重试失败的函数调用,适用于网络操作等场景。
输入校验:在执行前验证函数参数。
Python 装饰器总结
装饰器可以在不直接使用子类或修改被装饰函数源代码的情况下,动态改变函数、方法或类的功能。在 Python 中使用装饰器还能确保代码遵循 DRY(Don’t Repeat Yourself,不要重复自己)原则。装饰器的常见用例如下:
- 在 Flask 与 Django 等 Python 框架中进行授权
- 日志记录
- 执行时间测量
- 同步
想进一步了解 Python 装饰器,请参阅 Python 的装饰器库。
FAQs
使用装饰器时有哪些性能方面的考量?
会的。装饰器会引入额外的函数调用,因此会带来一定开销。当性能至关重要时,尤其是在性能敏感的场景下频繁调用被装饰函数时,应当考虑这部分开销。
装饰器可以用于类方法吗?如何使用?
可以。装饰器可像作用于普通函数那样应用于类方法。装饰器会接收该方法作为参数,并返回一个新的或经过修改的方法。这通常用于日志记录、访问控制或前置条件校验。
装饰器如何用于日志记录?
装饰器可通过在函数执行前后插入记录逻辑,来记录函数调用、其参数与返回值,并写入日志系统。这对追踪与调试很有帮助。
装饰器中的 @ 符号有什么意义?
@ 符号是 Python 中的语法糖,用于简化为函数应用装饰器。它允许您在函数定义上方直接应用装饰器,使代码更简洁、可读性更强。
装饰器可以修改函数的返回值吗?如何实现?
可以。装饰器可通过在包装函数中修改返回语句来改变函数的返回值。例如,可以转换输出数据类型、格式化结果,或在返回最终结果前执行额外处理。
当嵌套函数访问外层函数变量时,Python 如何处理变量作用域?
Python 采用 LEGB(局部、外层、全局、内建)作用域规则。对于嵌套函数,内层函数可以访问其外层函数作用域中的变量,这就形成闭包:即使外层函数已执行完毕,内层函数仍能访问外层变量。
闭包与装饰器有什么区别?
闭包是能记住外部作用域变量的函数。装饰器则是利用闭包来增强或包装其他函数的函数。
何时应使用 functools.wraps?
在编写装饰器时应始终使用 functools.wraps。它会保留原函数的元数据(如名称与文档字符串),有助于调试与文档化。
装饰器可以接收参数吗?
可以!您可以通过再包一层函数(装饰器工厂)向装饰器传参,从而自定义装饰器行为。
基于类的装饰器有什么优势?
基于类的装饰器可在多次调用间维护状态,并以面向对象的方式组织更复杂的逻辑。
装饰器只适用于函数吗?
不止用于函数。也可以用于类与方法。在 Flask 与 Django 等框架中,装饰器常用于路由、视图与模型。