Python 装饰器使用入门

本教程将向你介绍 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 语法来封装 displaydisplay_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_callmeasure_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 中的装饰器提供了一种简洁而强大的方法来扩展函数的功能。通过了解一等函数和闭包的概念,你便能深入理解装饰器背后的工作机制。

无论你是使用基于函数的装饰器还是基于类的装饰器,都能在不修改原有代码的基础上扩展函数的功能,这样既有助于维护代码的整洁性,也有助于提高代码可维护性。

相关推荐
叫我:松哥8 分钟前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
海里真的有鱼10 分钟前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
工业甲酰苯胺21 分钟前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
Eiceblue38 分钟前
Python 复制Excel 中的行、列、单元格
开发语言·python·excel
NLP工程化1 小时前
对 Python 中 GIL 的理解
python·gil
新知图书1 小时前
Rust编程的作用域与所有权
开发语言·后端·rust
极客代码1 小时前
OpenCV Python 深度指南
开发语言·人工智能·python·opencv·计算机视觉
liO_Oil1 小时前
(2024.9.19)在Python的虚拟环境中安装GDAL
开发语言·python·gdal安装
奈斯。zs1 小时前
yjs08——矩阵、数组的运算
人工智能·python·线性代数·矩阵·numpy
Melody20501 小时前
tensorflow-dataset 内网下载 指定目录
人工智能·python·tensorflow