本教程将向你介绍 Python 装饰器,包括其定义、工作原理以及在何种情况下应用它。
装饰器是一种强大而优雅的方式,可以在不修改其代码的情况下扩展函数或方法的功能。但在学习装饰器之前,我们需要掌握 Python 中的两个基本概念:一等函数(First-Class Functions)和闭包(Closure)。
基础知识
了解一等函数
在 Python 中,函数被视为一等对象,这意味着函数享有与其他数据类型(如整数、浮点数、字符串等)相同的待遇和权力。具体来说,一等函数具有以下几个关键特点:
- 可以作为参数传递给其他函数。
- 可以作为返回值从其他函数中返回。
- 可以存储在变量中。
- 可以包含在数据结构中 。
了解闭包
在 Python 中,闭包通常是通过在一个函数内部定义另一个函数,并返回这个函数的引用来创建的。在内部函数中,可以引用外部函数的参数和局部变量,这些引用会在内部函数中被保留。
让我们看一个示例来理解闭包:
csharp
def outer_func():
greet = "Hello!"
def inner_func():
print(greet)
return inner_func
new_function = outer_func()
new_function() # 输出: Hello!
new_function() # 输出: Hello!
在此示例中:
- 我们定义了一个函数
outer_func
,它不接受任何参数,并且拥有一个局部变量greet
。 - 在
outer_func
中定义了一个内部函数inner_func
,用于打印变量greet
。 - 当我们调用外部函数
outer_func
时,它将返回内部函数inner_func
,而内部函数并不会立即执行。我们将返回的内部函数inner_func
赋值给变量new_function
。然后,就可以像函数一样调用变量new_function
,它会记住outer_func
作用域中的greet
变量,并在每次调用时打印"Hello!"。
带参数的闭包
让我们通过向 outer_func
传递一个参数来增强我们的闭包,而不是使用局部变量:
scss
def outer_func(greet):
def inner_func():
print(greet)
return inner_func
namaste_func = outer_func("Namaste!")
howdy_func = outer_func("Howdy!")
namaste_func() # 输出: Namaste!
howdy_func() # 输出: Howdy!
改进如下:
- 我们重新定义外部函数
outer_func
,使其接受一个参数greet
。 - 内部函数
inner_func
打印参数greet
。 - 当我们用 "Namaste!" 和 "Howdy!" 调用
outer_func
时,它会返回已记住传入参数的inner_func
函数。
以上就是关于一等函数和闭包的简要介绍。如果你想了解更多,可以自行搜索查阅相关文章。
Python 装饰器介绍
装饰器本质上是一个函数,它可以接受一个函数作为参数并返回一个新的函数,这个新函数会在原函数执行前后添加特定的功能。
现在,让我们来看一个装饰器示例:
ruby
def decorator_function(func):
def wrapper_function():
return func()
return wrapper_function
在此代码中,我们定义了一个最简单的装饰器,它接受的参数不是一个值,而是一个函数。在内部函数wrapper_function
中,我们不会像闭包示例中那样直接打印出一条信息,而是要执行参数func
,然后返回func
的信息。
应用装饰器
以下是展示如何将装饰器应用于一个简单函数的示例:
ruby
def decorator_function(func):
def wrapper_function():
return func()
return wrapper_function
def display():
print('The display function was called')
decorated_display = decorator_function(display)
decorated_display() # 输出: The display function was called
在此示例中:
- 首先我们定义一个简单的函数
display
来打印一条信息。 - 然后我们使用装饰器
decorator_function
封装display
函数,并将封装后的结果赋值给一个新变量decorated_display
。 - 当我们调用变量
decorated_display()
时,它会首先运行装饰器中的包装函数wrapper_function
,然后再由包装函数调用display
函数,并返回其结果。
使用 @ 语法
Python 提供了一种使用 @
符号来应用装饰器的更易读的语法。这种语法更加直观,并且在 Python 编程中得到了更广泛的使用:
python
def decorator_function(func):
def wrapper_function():
print(f'Wrapper executed before {func.__name__}')
return func()
return wrapper_function
@decorator_function
def display():
print('The display function was called')
display() # 输出: Wrapper executed before display
# The display function was called
在此示例中:
- 我们在
display
函数定义时使用@decorator_function
语法对其进行封装,这相当于display = decoratorFunction(display)
。 - 当我们调用
display()
时,它会首先运行装饰器的封装函数打印附加信息。
如何处理带参数的函数
如果我们的函数接受参数,我们目前编写的装饰器将无法正常工作。例如,下面的函数:
python
def display_info(name, age):
print('display_info was called with ({}, {})'.format(name, age))
display_info('Kalam', 83) # 输出: display_info was called with (Kalam, 83)
如果我们尝试将之前的装饰器 decorator_function
应用到 display_info
,就会引发错误,因为包装函数 wrapper_function
在调用 display_info
时,并未向其传递参数。
修改装饰器以支持参数
通过使用 *args
和 **kwargs
,我们可以对装饰器进行改造,使其能够接收任意数量的位置参数和关键字参数。
python
import functools
def decoratorFunction(func):
@functools.wraps(func)
def wrapperFunction(*args, **kwargs):
print('Wrapper executed before {}'.format(func.__name__))
return func(*args, **kwargs)
return wrapperFunction
@decoratorFunction
def display():
print('The display function was called')
@decoratorFunction
def display_info(name, age):
print('display_info was called with ({}, {})'.format(name, age))
display_info('Kalam', 83)
display()
在这个更新的装饰器中:
wrapperFunction
现在可接受任意数量的位置参数 (*args
) 和关键字参数 (**kwargs
)。- 这些参数将在调用
wrapperFunction
时传递给func
。
其输出结果为:
sql
Wrapper executed before display_info
display_info was called with (Kalam, 83)
Wrapper executed before display
The display function was called
这种设计让我们的装饰器能够适应更多函数,无论其参数形式是什么和数量多少。
使用类作为装饰器
虽然大多数装饰器是基于函数,但利用类同样可以构建装饰器。采用类构建装饰器能够提供更大的灵活性和更好的可读性,这在处理复杂情况时尤为显著。
为了更进一步对装饰器的学习,我们将把之前基于函数的装饰器改为基于类的装饰器。
基于函数的装饰器
让我们从一个简单的基于函数的装饰器开始:
python
def decoratorFunction(func):
def wrapperFunction(*args, **kwargs):
print('Wrapper executed before calling {}'.format(func.__name__))
return func(*args, **kwargs)
return wrapperFunction
创建基于类的装饰器
要将基于函数的装饰器转换为基于类的装饰器,请按照以下步骤操作:
第一步:创建装饰器类
首先,我们创建一个名为 DecoratorClass
的新类,该类负责处理装饰器逻辑。
kotlin
class DecoratorClass:
pass
第二步:实现 __init__
方法
__init__
是一种特殊方法,用于在创建类的实例时初始化对象。
接下来,我们将要封装的函数(func
)作为参数传递给 __init__
方法,并将其保存在示例变量 self.func
中。
ruby
class DecoratorClass:
def __init__(self, func):
self.func = func
第三步:实现 __call__
方法
__call__
是一种特殊方法,它允许以函数的形式调用类的实例。这个方法至关重要,因为它负责实现装饰器的核心逻辑。
在本例中,__call__
方法使用 *args
和 **kwargs
来支持任意数量的位置参数和关键字参数。
ruby
class DecoratorClass:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print('Executing wrapper before {}'.format(self.func.__name__))
return self.func(*args, **kwargs)
使用基于类的装饰器
现在,我们可以使用 @
语法将基于类的装饰器应用于函数,就像使用基于函数的装饰器一样。
less
@DecoratorClass
def display():
print('display function executed')
@DecoratorClass
def display_info(name, age):
print('display_info function executed with arguments ({}, {})'.format(name, age))
运行函数
当我们调用封装后的函数时,会首先执行 DecoratorClass
的 __call__
方法。
scss
display_info('Kalam', 83)
display()
完整示例
下面是基于类的装饰器的完整代码:
python
class DecoratorClass:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print('Executing wrapper before {}'.format(self.func.__name__))
return self.func(*args, **kwargs)
@DecoratorClass
def display():
print('display function executed')
@DecoratorClass
def display_info(name, age):
print('display_info function executed with arguments ({}, {})'.format(name, age))
display_info('Kalam', 83)
display()
在这个基于类的装饰器中:
__init__
方法: 该方法负责将封装函数保存到类的实例上。__call__
方法: 该方法使得DecoratorClass
的实例能够被作为函数调用。它会首先打印一条信息,然后以传入的参数执行封装函数。
以上代码中,我们使用 @DecoratorClass
语法来封装 display
和 display_info
这两个函数。当调用 display_info('Kalam',83)
时,会首先执行 DecoratorClass
的 __call__
方法,打印信息,然后执行 display_info
。同样,当 display()
被调用时,它会首先执行 DecoratorClass
的 __call__
方法,打印信息,然后执行 display
。
基于函数的装饰器和基于类的装饰器都能提供相同的功能,如何选择取决于个人偏好和逻辑的复杂程度。
使用装饰器的最佳实践
在 Python 中使用装饰器时,必须遵循最佳实践,以保持代码的简洁和可维护性,并与 Pythonic 惯例保持一致。
1. 使用 functools.wraps
保留原始函数的元信息
使用装饰器时,原始函数的元数据(如名称和文档字符串)往往会丢失。这可能会导致混乱,并给自省和调试带来问题。为了保留这些元数据,请在封装函数中使用 functools.wraps
装饰器。
functools.wraps
是 Python 标准库中的一个装饰器(是的,你没有看错,functools.wraps()
本身就是一个装饰器),它通过复制原始函数的函数名、文档字符串和其他属性等来更新封装函数,使其看起来更像原始函数。
让我们来看一个例子:
python
import functools
def decoratorFunction(func):
@functools.wraps(func)
def wrapperFunction(*args, **kwargs):
print(f'Wrapper executed before {func.__name__}')
return func(*args, **kwargs)
return wrapperFunction
@decoratorFunction
def display():
"""Display function docstring"""
print('The display function was called')
print(display.__name__) # 输出: display
print(display.__doc__) # 输出: Display function docstring
在本例中,使用 @functools.wraps(func)
来确保 wrapperFunction
保留原始函数 func
的元数据。
2. 保持装饰器的简洁
一个装饰器应该只负责一件事,不应该尝试做太多事情。如果一个装饰器变得复杂,可以考虑将其分解为多个更简单的装饰器,并将它们组合在一起。例如:
python
import functools
def log_function_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f'Calling {func.__name__}')
return func(*args, **kwargs)
return wrapper
def measure_time(func):
import time
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'{func.__name__} took {end - start} seconds')
return result
return wrapper
@log_function_call
@measure_time
def compute_square(n):
return n * n
print(compute_square(5))
在本例中,log_function_call
和 measure_time
都是简单的单一功能的装饰器,我们可以组合使用这两个装饰器,为 compute_square
函数同时提供日志记录和计时功能。这就是装饰器的魅力所在------以一种简洁且易于理解的方式,实现通用功能的复用。
3. 为装饰器和包装函数合理命名
确保为装饰器及其封装函数选择能够明确其用途和作用的名字,这样有助于提升代码的可读性,让功能意图和行为一目了然。
4. 书写注解
始终为你的装饰器书写注解,解释其用途和使用方法。如果其他人会使用你的装饰器的话,这一点尤为重要。
应用场景
如果你还不知道何时应该使用装饰器,下面是我列举一些典型的使用场景,供你参考:
- 日志记录: 跟踪函数和方法的调用情况,尤其是在调试和监控应用程序时非常有用。
- 计时器: 监控函数的运行时长,这对于进行性能分析和优化很有用。
我们在 "最佳实践" 部分已经实现了上述两个例子。
除此之外,还有一些其他例子,例如:
- 参数验证: 用于验证函数的参数输入,确保只有输入符合特定条件的参数才能继续执行函数。
python
from functools import wraps
def validate_non_negative(func):
@wraps(func)
def wrapper(*args, **kwargs):
if any(arg < 0 for arg in args):
raise ValueError("Arguments must be non-negative")
return func(*args, **kwargs)
return wrapper
@validate_non_negative
def square_root(x):
return x ** 0.5
print(square_root(4))
- 结果缓存: 对需要大量计算的函数调用结果进行缓存,当遇到相同输入时,直接返回缓存中的结果,以提升效率。
less
import functools
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args in cache:
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
@memoize
def fibonacci(n):
if n in {0, 1}:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
fibonacci(10)
- 访问控制和身份验证: 在网络应用程序中,访问控制和身份验证对安全性至关重要。我们可以利用装饰器执行用户权限检查,确保只有授权用户才能够访问特定功能。
python
from functools import wraps
def requires_login(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not user_is_logged_in():
raise Exception("User not logged in")
return func(*args, **kwargs)
return wrapper
@requires_login
def view_dashboard():
return "Dashboard content"
# user_is_logged_in is a placeholder for the actual authentication check function.
结语
Python 中的装饰器提供了一种简洁而强大的方法来扩展函数的功能。通过了解一等函数和闭包的概念,你便能深入理解装饰器背后的工作机制。
无论你是使用基于函数的装饰器还是基于类的装饰器,都能在不修改原有代码的基础上扩展函数的功能,这样既有助于维护代码的整洁性,也有助于提高代码可维护性。