Skip to content

11 装饰器(⚠️ 人工关注)

📋 资料说明

  • 来源:课程 day10 后半
  • 笔记尚硅谷大模型技术之Python1.0.docx 第 12 章
  • 每日一考day10_每日一考.md
  • 视频:❌ 跳过(共 8 个视频:装饰器介绍 → 闭包实现 → 语法糖 → 多层装饰器 → 带参数 → 类装饰器)

🟢 🤖 AI 替代率:82%

为什么是这个水平:

装饰器的语法@decorator)AI 可以完美生成。但以下核心难点需要人工理解:

  • 装饰器 = 闭包 + 函数对象:装饰器本质是一个接受函数、返回函数的函数。不理解闭包就不理解装饰器。
  • 多层装饰器的执行顺序:这是最大的认知陷阱。装饰顺序是"离函数近的先装饰",而执行顺序相反——"离函数近的后执行"。违反直觉。
  • functools.wraps:被装饰后函数的 __name____doc__ 会被替换,需要用 @wraps 保留元数据。这不是语法规则,而是工程实践中的"坑"。

👤 人工干预率:18% | 人类重点关注

这是本模块最高人工干预率的章节。 核心原因:

  1. 函数是 Python 的一等公民:装饰器依赖这个前提。如果你来自 Java,需要建立"函数本身也是对象、可以传来传去"的心智模型。
  2. 多层装饰器执行流程:装饰是从下往上,执行是从上往下。很多人需要画出流程图才能理解。
  3. 带参数装饰器的两层嵌套@decorator(args) 实际上是 @decorator(args)decorator(args) 返回一个装饰器 → 这个装饰器再装饰函数。两层抽象。

人类与 AI 协作建议:

  1. 让 AI 生成所有装饰器的示例代码
  2. 手动执行下一节的"多层装饰器流程图",在纸上画出装饰顺序和调用顺序
  3. 任何 @ 符号在代码中出现时,想想它背后发生了什么
  4. print 在每个函数入口输出 __name__ 来验证执行顺序

1. 装饰器基础

装饰器是一种"在不修改原函数代码的情况下,给函数添加新功能"的机制。

闭包实现装饰器

python
# 装饰器就是一个接受函数、返回新函数的函数
def my_decorator(func):
    def wrapper():
        print("调用前的操作")  # 在原函数前执行
        func()                 # 调用原函数
        print("调用后的操作")  # 在原函数后执行
    return wrapper

# 手动使用装饰器
def say_hello():
    print("Hello!")

decorated = my_decorator(say_hello)
decorated()
# 输出:
# 调用前的操作
# Hello!
# 调用后的操作

语法糖 @

python
def my_decorator(func):
    def wrapper():
        print("before")
        func()
        print("after")
    return wrapper

@my_decorator         # 等价于 say_hello = my_decorator(say_hello)
def say_hello():
    print("Hello!")

say_hello()
# 输出:
# before
# Hello!
# after

装饰带参数的函数

python
def decorator(func):
    def wrapper(*args, **kwargs):
        print(f"调用 {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"返回: {result}")
        return result
    return wrapper

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

result = add(3, 5)
# 输出:
# 调用 add((3, 5), {})
# 返回: 8

functools.wraps

python
import functools

def decorator(func):
    @functools.wraps(func)  # 保留原函数的元信息
    def wrapper(*args, **kwargs):
        """包装函数"""
        print("before")
        return func(*args, **kwargs)
    return wrapper

@decorator
def say_hello():
    """打招呼"""
    print("Hello!")

print(say_hello.__name__)  # say_hello(不加 wraps 会输出 wrapper)
print(say_hello.__doc__)   # 打招呼(不加 wraps 会输出 包装函数)

2. 多层装饰器(⚠️ 重点)

python
def decorator_a(func):
    print("装饰器 A 开始装饰")
    def wrapper():
        print("A 执行前")
        func()
        print("A 执行后")
    return wrapper

def decorator_b(func):
    print("装饰器 B 开始装饰")
    def wrapper():
        print("B 执行前")
        func()
        print("B 执行后")
    return wrapper

# 等价于:say = decorator_a(decorator_b(say))
@decorator_a
@decorator_b
def say():
    print("--- 原函数 ---")

print("\n开始调用:")
say()

输出结果:

装饰器 B 开始装饰    # 先装饰 B(靠近函数的先装饰)
装饰器 A 开始装饰    # 再装饰 A

开始调用:
A 执行前              # A 的 wrapper
B 执行前              # B 的 wrapper
--- 原函数 ---        # 原函数
B 执行后              # B 的 wrapper(恢复)
A 执行后              # A 的 wrapper(恢复)

理解口诀

装饰从下往上,调用从上往下。 或者记住"洋葱模型"——函数在最里层,装饰器一层层包在外面。


3. 带参数的装饰器

python
def repeat(n):
    """
    带参数的装饰器——实际上是一个返回装饰器的函数
    """
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

# @repeat(3) 等价于 say_hello = repeat(3)(say_hello)
@repeat(3)
def say_hello(name):
    print(f"Hello, {name}!")

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

4. 类装饰器

python
class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"第 {self.count} 次调用")
        return self.func(*args, **kwargs)

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

say_hello()  # 第 1 次调用
say_hello()  # 第 2 次调用
say_hello()  # 第 3 次调用

5. 装饰器的实用场景

python
import functools
import time

# 1. 计时
def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} 耗时: {elapsed:.4f}s")
        return result
    return wrapper

# 2. 登录检查
def login_required(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if not is_logged_in():
            raise PermissionError("请先登录")
        return func(*args, **kwargs)
    return wrapper

# 3. 缓存
def cache(func):
    memo = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in memo:
            memo[args] = func(*args)
        return memo[args]
    return wrapper

练习题

练习 1:日志装饰器

写一个装饰器,在函数调用前后打印"开始"和"结束"日志。

练习 2:限次调用

写一个装饰器 max_calls(n),限制函数最多只能被调用 n 次,超过抛出异常。

练习 3:验证执行顺序

定义两个装饰器 @bold@italic,装饰一个 hello() 函数,观察输出顺序。


参考答案

练习 1:

python
def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[开始] {func.__name__}")
        result = func(*args, **kwargs)
        print(f"[结束] {func.__name__}")
        return result
    return wrapper

练习 2:

python
def max_calls(n):
    def decorator(func):
        count = 0
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal count
            count += 1
            if count > n:
                raise RuntimeError(f"{func.__name__} 调用次数已超限 ({n})")
            return func(*args, **kwargs)
        return wrapper
    return decorator

练习 3:

python
def bold(func):
    def wrapper():
        return f"<b>{func()}</b>"
    return wrapper

def italic(func):
    def wrapper():
        return f"<i>{func()}</i>"
    return wrapper

@bold
@italic
def hello():
    return "Hello"

print(hello())  # <b><i>Hello</i></b>
# 先 italic ,再 bold(靠近函数先装饰)

OPC 超级个体实战指南