Python教程19:装饰器入门

“简洁的力量在于抽象的恰当。”

装饰器是Python最强大的特性之一,也是初学者觉得最"神秘"的部分。今天我们从基础开始,逐步揭开装饰器的面纱,理解它的本质和应用。

1. 什么是装饰器

问题场景

假设你有多个函数,需要在每个函数执行前后记录日志:

 1def add(a, b):
 2    print("函数开始执行")  # 重复代码
 3    result = a + b
 4    print("函数执行完毕")  # 重复代码
 5    return result
 6
 7def multiply(a, b):
 8    print("函数开始执行")  # 重复代码
 9    result = a * b
10    print("函数执行完毕")  # 重复代码
11    return result

问题:每个函数都要写重复的日志代码,违反了DRY(Don’t Repeat Yourself)原则。

装饰器的解决方案

装饰器是一种设计模式,用于在不修改原函数代码的情况下,给函数添加新功能。就像给礼物包装一样,外面包了一层,但礼物本身没变。

 1@log_decorator  # 这就是装饰器
 2def add(a, b):
 3    return a + b
 4
 5# 调用时自动有日志功能
 6add(3, 5)
 7# 输出:
 8# 函数开始执行
 9# 8
10# 函数执行完毕

2. 理解装饰器的基础:闭包

在学习装饰器前,需要先理解闭包(Closure)

什么是闭包

闭包是指:一个函数内部定义的函数,可以访问外部函数的变量。

 1def outer(x):
 2    # x是外部函数的变量
 3    
 4    def inner(y):
 5        # inner可以访问outer的变量x
 6        return x + y
 7    
 8    return inner  # 返回内部函数
 9
10# 创建闭包
11add_5 = outer(5)
12print(add_5(3))  # 8(3 + 5)
13print(add_5(10)) # 15(10 + 5)

关键点

  • inner函数"记住"了外部函数的变量x
  • 即使outer执行完毕,x的值仍然保留
  • 这就是"闭包"——函数+环境

闭包的实际应用

 1def make_multiplier(n):
 2    """创建一个乘法器"""
 3    def multiplier(x):
 4        return x * n
 5    return multiplier
 6
 7# 创建不同的乘法器
 8times_3 = make_multiplier(3)
 9times_5 = make_multiplier(5)
10
11print(times_3(10))  # 30
12print(times_5(10))  # 50

为什么需要闭包

  • 数据封装:变量n被"保护"在闭包内
  • 工厂模式:根据参数创建不同的函数
  • 装饰器的基础:装饰器本质上就是闭包

3. 第一个装饰器

手动实现装饰器

 1def my_decorator(func):
 2    """
 3    这是一个装饰器函数
 4    - 接收一个函数作为参数
 5    - 返回一个新的函数
 6    """
 7    def wrapper(*args, **kwargs):
 8        print("函数执行前")
 9        result = func(*args, **kwargs)  # 调用原函数
10        print("函数执行后")
11        return result
12    return wrapper
13
14# 不使用@语法
15def say_hello():
16    print("Hello!")
17
18# 手动装饰
19say_hello = my_decorator(say_hello)
20
21say_hello()
22# 输出:
23# 函数执行前
24# Hello!
25# 函数执行后

使用@语法糖

Python提供了@符号作为语法糖,让装饰器使用更简洁:

1@my_decorator  # 等价于:say_hello = my_decorator(say_hello)
2def say_hello():
3    print("Hello!")
4
5say_hello()

@符号的本质

  • @my_decorator是一个快捷方式
  • 在函数定义后立即调用my_decorator(say_hello)
  • 用返回的新函数替换原函数

4. 带参数的装饰器

装饰带参数的函数

 1def log_decorator(func):
 2    """记录函数调用的装饰器"""
 3    def wrapper(*args, **kwargs):
 4        print(f"调用函数:{func.__name__}")
 5        print(f"参数:args={args}, kwargs={kwargs}")
 6        result = func(*args, **kwargs)
 7        print(f"返回值:{result}")
 8        return result
 9    return wrapper
10
11@log_decorator
12def add(a, b):
13    return a + b
14
15result = add(3, 5)
16# 输出:
17# 调用函数:add
18# 参数:args=(3, 5), kwargs={}
19# 返回值:8

*args, **kwargs的作用

  • 让wrapper能接收any参数
  • 透传给原函数
  • 保持装饰器的通用性

装饰器本身带参数

如果装饰器需要配置参数,需要再包一层函数:

 1def repeat(times):
 2    """
 3    可配置的装饰器
 4    - times: 重复执行次数
 5    - 这是装饰器工厂,返回真正的装饰器
 6    """
 7    def decorator(func):
 8        def wrapper(*args, **kwargs):
 9            for _ in range(times):
10                result = func(*args, **kwargs)
11            return result
12        return wrapper
13    return decorator
14
15@repeat(times=3)  # 先调用repeat(3)得到装饰器,再装饰say_hello
16def say_hello():
17    print("Hello!")
18
19say_hello()
20# 输出:
21# Hello!
22# Hello!
23# Hello!

三层结构

  1. repeat(times):接收配置参数
  2. decorator(func):真正的装饰器
  3. wrapper(*args, **kwargs):包装函数

5. 实用装饰器示例

计时装饰器

 1import time
 2
 3def timer(func):
 4    """
 5    测量函数执行时间的装饰器
 6    - 使用time模块(Python标准库)
 7    - time.time()返回当前时间戳(秒)
 8    """
 9    def wrapper(*args, **kwargs):
10        start = time.time()
11        result = func(*args, **kwargs)
12        end = time.time()
13        print(f"{func.__name__}执行时间:{end - start:.4f}秒")
14        return result
15    return wrapper
16
17@timer
18def slow_function():
19    time.sleep(2)  # 模拟耗时操作
20    return "完成"
21
22slow_function()
23# 输出:slow_function执行时间:2.0001秒

缓存装饰器

 1def cache(func):
 2    """
 3    简单的缓存装饰器
 4    - 使用字典存储已计算的结果
 5    - 避免重复计算,提升性能
 6    - Python标准库有functools.lru_cache,功能更强
 7    """
 8    cached_results = {}
 9    
10    def wrapper(*args):
11        if args in cached_results:
12            print(f"从缓存返回:{args}")
13            return cached_results[args]
14        
15        result = func(*args)
16        cached_results[args] = result
17        return result
18    
19    return wrapper
20
21@cache
22def fibonacci(n):
23    """计算斐波那契数"""
24    if n <= 1:
25        return n
26    return fibonacci(n-1) + fibonacci(n-2)
27
28print(fibonacci(10))  # 第一次计算
29print(fibonacci(10))  # 第二次从缓存返回

权限检查装饰器

 1def require_auth(func):
 2    """
 3    权限检查装饰器
 4    - 模拟需要登录才能执行的函数
 5    - Web开发中常用于路由保护
 6    """
 7    def wrapper(*args, **kwargs):
 8        # 假设这里检查用户是否登录
 9        is_authenticated = False  # 模拟未登录
10        
11        if not is_authenticated:
12            print("错误:需要登录")
13            return None
14        
15        return func(*args, **kwargs)
16    
17    return wrapper
18
19@require_auth
20def view_profile():
21    return "个人资料页面"
22
23view_profile()  # 输出:错误:需要登录

6. 多个装饰器

可以给一个函数应用多个装饰器:

1@decorator1
2@decorator2
3@decorator3
4def my_function():
5    pass
6
7# 等价于:
8# my_function = decorator1(decorator2(decorator3(my_function)))

执行顺序:从下到上装饰,从上到下执行。

 1def bold(func):
 2    def wrapper():
 3        return "<b>" + func() + "</b>"
 4    return wrapper
 5
 6def italic(func):
 7    def wrapper():
 8        return "<i>" + func() + "</i>"
 9    return wrapper
10
11@bold
12@italic
13def greet():
14    return "Hello"
15
16print(greet())  # <b><i>Hello</i></b>

7. 保持函数元信息

装饰后,函数的元信息会丢失:

 1def my_decorator(func):
 2    def wrapper(*args, **kwargs):
 3        return func(*args, **kwargs)
 4    return wrapper
 5
 6@my_decorator
 7def my_function():
 8    """这是我的函数"""
 9    pass
10
11print(my_function.__name__)  # wrapper(而不是my_function)
12print(my_function.__doc__)   # None(文档丢失)

解决方案:使用functools.wraps

 1from functools import wraps
 2
 3def my_decorator(func):
 4    @wraps(func)  # 保留原函数的元信息
 5    def wrapper(*args, **kwargs):
 6        return func(*args, **kwargs)
 7    return wrapper
 8
 9@my_decorator
10def my_function():
11    """这是我的函数"""
12    pass
13
14print(my_function.__name__)  # my_function
15print(my_function.__doc__)   # 这是我的函数

functools.wraps的作用

  • 复制原函数的__name____doc__等属性
  • 让装饰后的函数看起来像原函数
  • 是装饰器的最佳实践

8. 类装饰器

除了函数,类也可以作为装饰器:

 1class CountCalls:
 2    """
 3    统计函数调用次数的装饰器(类版本)
 4    - 使用类可以更好地管理状态
 5    - __init__接收被装饰的函数
 6    - __call__让对象可以像函数一样调用
 7    """
 8    def __init__(self, func):
 9        self.func = func
10        self.count = 0
11    
12    def __call__(self, *args, **kwargs):
13        self.count += 1
14        print(f"调用次数:{self.count}")
15        return self.func(*args, **kwargs)
16
17@CountCalls
18def say_hello():
19    print("Hello!")
20
21say_hello()  # 调用次数:1
22say_hello()  # 调用次数:2

9. 小结

今天我们学习了装饰器:

  • 本质:函数接收函数,返回函数
  • 基础:闭包(函数+环境变量)
  • 语法@decorator是语法糖
  • 带参数:需要三层函数结构
  • 实用示例:计时、缓存、权限检查
  • 多装饰器:从下到上装饰
  • 最佳实践:使用@wraps保持元信息
  • 类装饰器:用类实现装饰器

装饰器是Python的高级特性,掌握它能让代码更优雅、更模块化。


练习题

  1. 写一个装饰器,记录函数被调用的时间和参数
  2. 实现一个重试装饰器,函数失败时自动重试3次
  3. 写一个装饰器,限制函数执行频率(如每秒最多1次)

本文代码示例

关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读