python的装饰器怎么使用

Python装饰器是一种强大的设计模式,它允许你在不修改原函数代码的前提下,动态地为其添加新功能,如日志记录、性能测试、权限校验等。其核心思想是将一个函数作为参数传递给另一个函数(装饰器),并返回一个新的函数。

一、基本语法与工作原理

装饰器的基本结构是一个接受函数作为参数并返回新函数的高阶函数。最直观的用法是使用 @装饰器名 语法糖,这等同于 原函数名 = 装饰器名(原函数名)

基础示例

复制代码
复制代码
def my_decorator(func):
    def wrapper():
        print("函数调用前执行")
        func()
        print("函数调用后执行")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()
# 输出:
# 函数调用前执行
# Hello!
# 函数调用后执行

在这个例子中,my_decorator 接收 say_hello 函数,并返回一个包装函数 wrapper。当调用 say_hello() 时,实际上执行的是 wrapper(),从而在原函数前后添加了额外操作。

二、处理带参数的函数

为了使装饰器能通用地装饰任何函数,包装函数应使用 *args**kwargs 来接收任意数量的位置参数和关键字参数。

通用装饰器示例

复制代码
复制代码
def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"正在调用函数: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"函数 {func.__name__} 执行完毕")
        return result
    return wrapper

@log_decorator
def add(a, b):
    return a + b

print(add(2, 3))
# 输出:
# 正在调用函数: add
# 函数 add 执行完毕
# 5

三、带参数的装饰器

有时你需要装饰器本身也能接受参数,这需要再嵌套一层函数。最外层函数接收装饰器参数,中间层接收被装饰的函数,最内层是最终的包装函数。

示例:重复执行函数

复制代码
复制代码
def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(num_times=3)
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")
# 输出:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

这里,repeat(3) 返回 decorator_repeat 装饰器,然后应用到 greet 函数上。

四、类装饰器

除了函数,类也可以作为装饰器,只需实现 __call__ 方法。

类装饰器示例

复制代码
复制代码
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        print(f"函数已被调用 {self.calls} 次")
        return self.func(*args, **kwargs)

@CountCalls
def example():
    print("执行示例函数")

example()
example()
# 输出:
# 函数已被调用 1 次
# 执行示例函数
# 函数已被调用 2 次
# 执行示例函数

类装饰器在初始化 (__init__) 时接收原函数,在调用 (__call__) 时执行装饰逻辑。

五、保留原函数元信息

使用装饰器后,原函数的 __name____doc__ 等元信息会被包装函数覆盖。使用 functools.wraps 装饰器可以保留这些信息,这是最佳实践。

复制代码
复制代码
from functools import wraps

def good_decorator(func):
    @wraps(func)  # 关键:将原函数的元信息复制到包装函数
    def wrapper(*args, **kwargs):
        print("装饰器逻辑")
        return func(*args, **kwargs)
    return wrapper

@good_decorator
def documented_func():
    """这是一个有文档字符串的函数。"""
    pass

print(documented_func.__name__)  # 输出:documented_func
print(documented_func.__doc__)   # 输出:这是一个有文档字符串的函数。

六、实际应用场景

装饰器在实际开发中用途广泛,以下是一些典型场景:

  1. 日志记录:自动记录函数的调用参数和返回值。
  2. 性能测试/计时:测量函数的执行时间。
  3. 权限验证:在执行业务逻辑前检查用户权限。
  4. 缓存:存储函数计算结果,避免重复计算以提高性能。
  5. 输入验证:确保传入函数的参数符合要求。

七、在类中使用装饰器

装饰器同样可以应用于类的方法,包括实例方法、类方法 (@classmethod) 和静态方法 (@staticmethod)。多个装饰器可以叠加使用,其应用顺序是从下往上(离函数定义最近的先应用)。

复制代码
复制代码
def decorator_a(func):
    def wrapper():
        print("装饰器A")
        func()
    return wrapper

def decorator_b(func):
    def wrapper():
        print("装饰器B")
        func()
    return wrapper

@decorator_a
@decorator_b
def my_func():
    print("原函数")

my_func()
# 输出:
# 装饰器A
# 装饰器B
# 原函数

总结

Python装饰器的本质是接受函数作为参数并返回函数的高阶函数 ,通过 @ 语法糖提供了一种优雅的"增强"函数功能的方式。掌握其基本结构、参数处理、元信息保留以及类装饰器等高级用法,能极大地提升代码的复用性、可读性和模块化程度。在实际使用时,建议保持装饰器逻辑简洁,并使用 @functools.wraps 来保留原函数的元信息。

my_decorator 接收 say_hello 函数,并返回一个包装函数 wrapper。当调用 say_hello() 时,实际上执行的是 wrapper(),不明白,返回的wrapper在哪里执行?


你的困惑非常典型,它触及了Python装饰器机制的核心。简单来说,wrapper函数是在你调用被装饰后的函数(例如 say_hello())时,在内存中动态创建并执行的。它并不是一个预先存在、等待被调用的独立函数,而是装饰器在"装饰阶段"为你量身定做的一个新函数。

为了让你彻底明白,我将这个过程拆解为两个关键阶段,并结合一个比喻来解释。

第一阶段:装饰阶段(定义时执行)

当你写下 @my_decorator 并定义 say_hello 函数时,Python解释器会立即执行以下操作:

  1. 定义原始函数 :首先,解释器在内存中创建了原始的 say_hello 函数对象。
  2. 执行装饰器 :然后,它看到 @my_decorator 语法糖,这等同于执行了 say_hello = my_decorator(say_hello)。此时,my_decorator函数被调用 ,并将原始的 say_hello 函数对象作为参数 func 传入。
  3. 创建并返回包装器 :在 my_decorator 函数内部,它定义了另一个函数 wrapper。注意,此时只是定义wrapper 函数,并未执行 它。最后,my_decorator 将这个新定义的 wrapper 函数对象返回
  4. 重新绑定名称 :最关键的一步发生了:返回的 wrapper 函数对象被赋值给了变量名 say_hello。于是,变量名 say_hello 不再指向最初的那个函数,而是指向了 my_decorator 返回的 wrapper 函数

这个阶段在模块加载或代码执行到定义处时就完成了,只发生一次 。你可以把它想象成给手机安装一个带滤镜功能的相机APP。安装过程(装饰阶段)就是替换了你手机里原来的相机图标(原始函数),让你以后点开的都是这个带滤镜的新APP(包装函数)。

第二阶段:调用阶段(调用时执行)

当你之后在代码中写下 say_hello() 并执行时,发生的事情就完全不同了:

  1. 找到函数对象 :Python解释器根据变量名 say_hello 去查找它对应的函数对象。由于第一阶段已经重新绑定,现在它找到的是 wrapper 函数。
  2. 执行包装器 :因此,你调用的 say_hello() 实际上是在调用 wrapper()。解释器开始执行 wrapper 函数体内的代码。
  3. 执行装饰逻辑wrapper 函数会先执行你添加的额外代码,比如打印 "Before function call"
  4. 调用原函数 :接着,在 wrapper 函数内部,通过 func()(这里的 func 就是第一阶段传入并被 wrapper 记住的原始 say_hello 函数对象)来调用最初的、未被装饰的函数逻辑,比如打印 "Hello!"
  5. 执行后续逻辑并返回 :最后,wrapper 执行完原函数后,继续执行剩余的装饰代码(如打印 "After function call"),并将原函数的返回值(如果有)作为自己的返回值返回。

这个阶段发生在你每次显式调用函数的时候。继续用手机相机的比喻,按下快门拍照(调用阶段) 时,你使用的是已经安装好的带滤镜APP(wrapper)。这个APP内部会先进行一些滤镜处理(装饰逻辑),然后调用底层的相机硬件功能(原始函数),最后把处理好的照片(返回值)交给你。

总结与验证

为了让你更直观地看到这两个阶段,请看下面的代码和输出:

复制代码
复制代码
def my_decorator(func):
    print("【装饰阶段】装饰器my_decorator被调用,正在包装函数...")
    def wrapper():
        print("【调用阶段】wrapper开始执行 -> 执行装饰逻辑")
        func()
        print("【调用阶段】wrapper结束执行")
    return wrapper

print("1. 开始定义被装饰的函数...")
@my_decorator
def say_hello():
    print("    Hello! (原始函数逻辑)")

print("2. 函数定义完成。")
print("3. 现在开始调用say_hello():")
say_hello()
print("4. 再次调用say_hello():")
say_hello()

输出结果将是:

复制代码
复制代码
1. 开始定义被装饰的函数...
【装饰阶段】装饰器my_decorator被调用,正在包装函数...
2. 函数定义完成。
3. 现在开始调用say_hello():
【调用阶段】wrapper开始执行 -> 执行装饰逻辑
    Hello! (原始函数逻辑)
【调用阶段】wrapper结束执行
4. 再次调用say_hello():
【调用阶段】wrapper开始执行 -> 执行装饰逻辑
    Hello! (原始函数逻辑)
【调用阶段】wrapper结束执行

从输出可以清晰地看到:

  • 装饰阶段的打印语句只出现了一次,在函数定义时就执行了。
  • 调用阶段 的打印语句在每次调用 say_hello() 时都会执行。

所以,wrapper 函数是装饰器在"装饰阶段"制造出来的一个"替身演员"。当你以后想找明星(原始函数)时,经纪人(变量名 say_hello)派出的都是这个替身(wrapper)。替身会先完成一些特技动作(装饰逻辑),再让真正的明星(通过 func() 调用)表演核心内容,最后把整个表演呈现给你。

相关推荐
2501_944875514 小时前
Go后端工程师
开发语言·后端·golang
该用户已不存在4 小时前
没有这7款工具,难怪你的Python这么慢
后端·python
听风吟丶4 小时前
Java 反射机制深度解析:从原理到实战应用与性能优化
java·开发语言·性能优化
serve the people4 小时前
tensorflow 零基础吃透:RaggedTensor 的不规则形状与广播机制 2
人工智能·python·tensorflow
Hello.Reader5 小时前
Flink ML 基本概念Table API、Stage、Pipeline 与 Graph
大数据·python·flink
chen_note5 小时前
Python面向对象、并发编程、网络编程
开发语言·python·网络编程·面向对象·并发编程
她说彩礼65万5 小时前
C# params使用
开发语言·c#·log4j
信看5 小时前
树莓派CAN(FD) 测试&&RS232 RS485 CAN Board 测试
开发语言·python
brent4235 小时前
DAY24推断聚类后簇的类型
python