34·模块八:装饰器与高级主题高级

装饰器基础(函数包装)

decoratorswrappers

Python 编程:从零基础到精通

第 34 课:装饰器基础(函数包装)

所属模块: 模块八:装饰器与高级主题 难度: Advanced 标签: decorators, wrappers


学习目标

完成本课学习后,你将能够:

  1. 理解装饰器的核心思想——"函数包装"或"函数增强"。
  2. 掌握如何从零开始编写一个简单的装饰器。
  3. 运用装饰器为函数添加通用的功能,例如日志记录和性能测量。
  4. 熟练使用 Python 的 @ 语法糖来应用装饰器。

核心概念

1. 什么是装饰器?

想象一下,你有一件基础款T恤(一个基础函数),但它有些单调。你给它添加了一个口袋(添加了新功能),这个过程就是"装饰"。在Python中,装饰器就是一个"函数工厂",它接收一个函数作为输入,然后返回一个被"增强"后的新函数。

装饰器本质上是一个函数,它让你可以在不修改原函数代码的情况下,给原函数增加额外的功能。

2. 装饰器的工作原理(手动包装)

在没有 @ 语法糖之前,我们可以通过手动包装来实现类似效果。关键在于高阶函数:函数可以作为参数传递,也可以作为返回值返回。

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

def my_decorator(func):
    # 这是一个包装器函数,它会替代原函数的功能
    def wrapper():
        print("Something is happening before the function is called.")
        func()  # 在这里调用原始函数
        print("Something is happening after the function is called.")
    return wrapper  # 返回包装器函数,而不是调用它

# 手动"装饰"函数
wrapped_say_hello = my_decorator(say_hello)
wrapped_say_hello()

输出:

Something is happening before the function is called.
Hello!
Something is happening after the function is called.

这里,my_decorator 就是一个装饰器。它"包装"了 say_hello 函数,创建了一个新的函数 wrapper,这个新函数在调用原函数前后都执行了额外代码。

3. 使用 @ 语法糖

Python 提供了 @ 语法,让上面的手动过程变得简洁优雅。下面两种写法是完全等价的:

# 写法一:使用@语法糖
@my_decorator
def say_hello():
    print("Hello!")

# 等价于 写法二:手动包装
say_hello = my_decorator(say_hello)

使用 @ 后,代码更清晰,意图更明确。


代码示例

示例1:一个简单的日志装饰器

这个装饰器会在每次调用函数时,打印出函数的名称和调用时间。

import functools
from datetime import datetime

def log_calls(func):
    """一个记录函数调用的装饰器"""
    # @functools.wraps(func) 是一个很重要的工具,它能保留原函数的元信息(如__name__, __doc__)
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        """这是包装器函数"""
        # 在函数被调用前记录信息
        print(f"[{datetime.now()}] Calling function '{func.__name__}'...")
        # 调用原始函数
        result = func(*args, **kwargs)
        # 在函数被调用后记录信息
        print(f"[{datetime.now()}] Function '{func.__name__}' finished.")
        return result
    return wrapper

# 使用@语法应用装饰器
@log_calls
def add(a, b):
    """返回两个数的和"""
    print(f"Computing {a} + {b}")
    return a + b

@log_calls
def greet(name):
    """向某人问好"""
    print(f"Hello, {name}!")

# 调用被装饰的函数
print("Test 1: add function")
result = add(3, 5)
print(f"Result: {result}\n")

print("Test 2: greet function")
greet("Alice")
print()

# 查看函数的元信息是否被保留
print(f"add function name: {add.__name__}")
print(f"add function docstring: {add.__doc__}")

输出示例(时间戳会变化):

Test 1: add function
[2023-10-27 10:00:00.123456] Calling function 'add'...
Computing 3 + 5
[2023-10-27 10:00:00.123456] Function 'add' finished.
Result: 8

Test 2: greet function
[2023-10-27 10:00:00.123456] Calling function 'greet'...
Hello, Alice!
[2023-10-27 10:00:00.123456] Function 'greet' finished.

add function name: add
add function docstring: 返回两个数的和

示例2:一个计时装饰器

这个装饰器用于测量函数执行所需的时间。

import time
import functools

def timer(func):
    """测量函数执行时间的装饰器"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        """这是包装器函数"""
        start_time = time.perf_counter() # 使用高精度计时器
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        execution_time = end_time - start_time
        print(f"'{func.__name__}' 运行时间: {execution_time:.4f} 秒")
        return result
    return wrapper

@timer
def slow_function(delay):
    """一个故意执行很慢的函数"""
    print(f"开始一个 {delay} 秒的延迟操作...")
    time.sleep(delay)
    return "操作完成!"

# 测试计时装饰器
result = slow_function(1.5)
print(f"函数返回结果: {result}")

输出:

开始一个 1.5 秒的延迟操作...
'slow_function' 运行时间: 1.5001 秒
函数返回结果: 操作完成!

实践练习

练习 1:基础装饰器修改(易)

要求: 修改上面的 log_calls 装饰器,让它在日志中额外打印出函数的参数信息(位置参数和关键字参数)。

预期输出示例

[时间] Calling function 'add' with args=(3, 5), kwargs={}
Computing 3 + 5
[时间] Function 'add' finished.
Result: 8

练习 2:编写一个"重试"装饰器(中)

要求: 编写一个名为 retry 的装饰器。它可以让被装饰的函数在发生异常时自动重试,最多重试 max_retries 次(默认为 3)。如果在重试次数内成功,则返回结果;如果用尽重试次数仍然失败,则抛出最后一次的异常。 提示:你需要在装饰器中捕获异常 (try...except)。

代码框架

def retry(max_retries=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 你的代码写在这里
            # 提示:可能需要一个 for 循环来重试,并处理异常
            pass
        return wrapper
    return decorator

# 测试你的装饰器
import random

@retry(max_retries=2)
def unstable_operation():
    """这个函数有70%的几率会失败"""
    if random.random() < 0.7:
        raise ValueError("操作失败!")
    return "操作成功!"

# 调用并观察重试行为
print(unstable_operation())

练习 3:组合装饰器思考(难)

要求: 思考以下代码的输出顺序,并写在注释里,然后运行代码验证你的想法。

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<b>" + func(*args, **kwargs) + "</b>"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return "<i>" + func(*args, **kwargs) + "</i>"
    return wrapper

@bold
@italic
def say(text):
    return text

# 输出顺序是怎样的?请写出你的答案,然后运行代码验证。
print(say("Hello"))

常见错误

  1. 忘记返回包装器函数:装饰器的核心是返回 wrapper 函数本身,而不是调用它。错误:return wrapper()。正确:return wrapper
  2. 丢失原函数信息:不使用 @functools.wraps(func),会导致被装饰函数的 __name____doc__ 等属性丢失,全部变成 wrapper 的信息。这不利于调试和自省。
  3. 装饰器内部逻辑错误:在 wrapper 函数内部,忘记调用原始的 func,导致原函数的核心功能丢失。
  4. 对带参数的函数束手无策:装饰器中的 wrapper 函数应使用 *args, **kwargs 来接收任意参数,以确保能包装任何签名的函数。这是编写通用装饰器的关键。
  5. 混淆 @decorator@decorator():当装饰器本身需要参数时,需要使用带括号的语法(如 @decorator(arg)),这实际上是调用了一个"装饰器工厂"函数。本课中 retry(max_retries=2) 就是这种情况。基础的无参装饰器用 @decorator

小结

在本课中,我们学习了 Python 装饰器的基础:

  • 核心思想:装饰器是一个接受函数并返回新函数的函数,用于在不修改原函数代码的情况下,为其添加额外功能。
  • 实现方式:通过定义一个内部函数 wrapper,在 wrapper 中调用原函数并加入额外逻辑。
  • 语法糖:使用 @decorator_name 语法可以简洁地将装饰器应用到函数上。
  • 最佳实践:始终使用 @functools.wraps(func) 来保留原函数的元信息,并使用 *args, **kwargs 使装饰器通用。
  • 应用场景:日志记录、性能测试(计时)、权限校验、输入验证、缓存等通用功能非常适合用装饰器来实现。

装饰器是Python中强大且优雅的特性,是理解许多高级框架(如 Flask, Django)源码的关键。掌握了基础,下一课我们将学习如何创建带参数的装饰器,让你的装饰器更加灵活强大!

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「带参数的装饰器」 以巩固所学知识。