目 录CONTENT

文章目录

Python(三十六) 装饰器与闭包完整教程

Python(三十六) 装饰器与闭包完整教程

目录

  1. 开篇:从"手机壳"理解装饰器
  2. 前置知识:函数是一等公民
  3. 闭包:函数携带记忆
  4. 装饰器入门:给函数穿上"外套"
  5. 被装饰函数的返回值与参数
  6. functools.wraps:保留函数元信息
  7. 带参数的装饰器:装饰器的"配置版"
  8. 多层装饰器:叠罗汉
  9. 类装饰器:用类实现的装饰器
  10. 内置装饰器:@staticmethod / @classmethod / @property
  11. 综合实战案例
  12. 常见面试题与易错点

一、开篇:从"手机壳"理解装饰器

1.1 生活类比

你去买了一部新手机(原始函数),为了防摔给它套了个手机壳(装饰器)。

  • 手机本身(原始函数):能打电话、发短信——功能完全没变
  • 套上手机壳(装饰器):手机还是那个手机,但多了防摔保护,还不影响打电话

更重要的是:朋友的手机也能套同款手机壳——装饰器可以重复使用

# 类比到代码:
原函数 = 裸机
@装饰器
原函数 = 套了壳的手机  →  功能不变,多了附加能力

1.2 本教程学习路线

函数是一等公民(函数可以当参数传、当返回值返回)
    │
    └── 闭包(内层函数记住了外层函数的变量)
            │
            └── 装饰器(闭包的典型应用:给函数加功能)
                    │
                    ├── 基础装饰器(无参数)
                    │
                    ├── functools.wraps(保留函数名和文档)
                    │
                    ├── 带参数的装饰器(三层嵌套)
                    │
                    ├── 多层装饰器(多个装饰器叠加)
                    │
                    └── 类装饰器(用 __call__ 实现)

二、前置知识:函数是一等公民

在 Python 中,函数是"一等公民"。这意味着函数可以:

  1. 赋值给变量
  2. 作为参数传给另一个函数
  3. 作为返回值从函数中返回
  4. 存储在数据结构(列表、字典)中
# 1. 函数可以赋值给变量
def say_hello(name):
    return f"你好,{name}!"

greet = say_hello          # 把函数赋值给变量(注意:没加括号!)
print(greet("小明"))       # 你好,小明!
print(greet)               # <function say_hello at 0x...>


# 2. 函数可以作为参数传给另一个函数
def execute(func, arg):
    """执行传入的函数"""
    print(f"即将执行 {func.__name__}...")   # __name__ 是函数的名字
    result = func(arg)
    print(f"执行完毕,结果:{result}")
    return result

execute(say_hello, "小红")
# 输出:
# 即将执行 say_hello...
# 执行完毕,结果:你好,小红!


# 3. 函数可以作为返回值
def create_multiplier(n):
    """返回一个'乘以 n'的函数"""
    def multiplier(x):
        return x * n
    return multiplier          # 返回内部函数(没加括号!)

double = create_multiplier(2)   # double 现在是一个函数
triple = create_multiplier(3)   # triple 也是函数
print(double(5))                # 10
print(triple(5))                # 15


# 4. 函数可以存储在数据结构中
func_list = [say_hello, lambda x: x.upper()]
func_dict = {"greet": say_hello, "upper": lambda x: x.upper()}

新手易踩坑:

  • :把函数赋值给变量时加了括号 → greet = say_hello() 会执行函数并把返回值赋给变量,不是把函数本身赋给变量!
  • 规则func 是函数本身;func() 是调用函数并获取返回值

课堂小练习 1

写一个函数 apply_twice(func, value),它把 funcvalue 上连续应用两次。例如 apply_twice(lambda x: x * 2, 3) 应该返回 12(3→6→12)。

点击查看参考答案
def apply_twice(func, value):
    return func(func(value))

print(apply_twice(lambda x: x * 2, 3))  # 12
print(apply_twice(lambda x: x + 5, 10)) # 20

三、闭包:函数携带记忆

3.1 什么是闭包?

生活类比:你妈妈给你做了个"专属记账本"——里面已经写好了你的名字(外部变量),你只需要每次填金额(参数)就行了。"记账本"带着你的名字,这就是闭包

技术定义:当一个内部函数引用了外部函数的变量,并且外部函数返回了这个内部函数,就形成了闭包(Closure)

def make_account(owner):       # 外部函数
    """创建一个属于特定主人的记账函数"""
    total = 0                  # 外部变量(自由变量)

    def add(amount):           # 内部函数(闭包)
        nonlocal total         # 声明使用外部的 total
        total += amount
        return f"{owner}的账本:存入{amount}元,余额{total}元"

    return add                 # 返回内部函数

# 创建两个独立的账本
xiaoming_account = make_account("小明")
xiaohong_account = make_account("小红")

print(xiaoming_account(100))   # 小明的账本:存入100元,余额100元
print(xiaoming_account(50))    # 小明的账本:存入50元,余额150元
print(xiaohong_account(200))   # 小红的账本:存入200元,余额200元
# 关键:小明和小红的账本是独立的!各自带着各自的 total 和 owner

3.2 闭包的条件

一个函数要成为闭包,必须满足三个条件:

  1. 嵌套函数:内部函数定义在外部函数里面
  2. 引用外部变量:内部函数使用了外部函数的变量(称为"自由变量")
  3. 返回内部函数:外部函数把内部函数作为返回值返回

3.3 图解闭包的内存模型

make_account("小明") 调用后:

┌─── make_account 作用域(已返回,但变量被闭包"抓住"了)───┐
│  owner = "小明"                                         │
│  total = 0  →  →  → (add 第一次调用) → total = 100     │
│                        (add 第二次调用) → total = 150     │
│                                                         │
│  ┌─── add 闭包 ───────────────────────────────────┐    │
│  │  def add(amount):                              │    │
│  │      nonlocal total       ← 引用外部 total      │    │
│  │      total += amount      ← 修改外部 total      │    │
│  │      return f"{owner}..." ← 引用外部 owner      │    │
│  └────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────┘

闭包的魔法:外部函数早已返回,但它的变量被内部函数"记住"了!

3.4 查看闭包捕获的变量

def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter = make_counter()
print(counter())  # 1
print(counter())  # 2

# 查看闭包"记住"了哪些变量
print(counter.__closure__)          # (<cell at 0x...: int object at 0x...>,)
print(counter.__code__.co_freevars) # ('count',)  ← 自由变量叫 count
print(counter.__closure__[0].cell_contents)  # 2  ← count 当前的值

3.5 闭包的经典应用场景

# 场景1:工厂函数(用参数定制行为)
def power_factory(exponent):
    """返回一个计算 power 的函数"""
    def power(base):
        return base ** exponent
    return power

square = power_factory(2)   # square(x) = x²
cube = power_factory(3)     # cube(x) = x³
print(square(4))  # 16
print(cube(4))    # 64


# 场景2:惰性求值(保存状态,延迟计算)
def lazy_sum(*args):
    """不立刻求和,而是返回一个'准备求和'的函数"""
    def calc():
        return sum(args)
    return calc

lazy = lazy_sum(1, 2, 3, 4, 5)
print("还没算呢...")
print(lazy())  # 现在才算!15


# 场景3:简化回调函数
def make_callback(message):
    """生成一个带着特定消息的回调函数"""
    def callback():
        print(f"回调触发:{message}")
    return callback

on_click = make_callback("按钮被点击了")
on_timeout = make_callback("连接超时")

on_click()     # 回调触发:按钮被点击了
on_timeout()   # 回调触发:连接超时

3.6 nonlocal 关键字

# 没有 nonlocal —— 会出错!
def broken_counter():
    count = 0
    def increment():
        count += 1  # ❌ UnboundLocalError!
        return count
    return increment

# 原因:count += 1 会先读取 count,Python 发现 count 在内部函数中被赋值
# 就认为它是一个局部变量。但 count 实际上来自外部。


# 方案1:用 nonlocal(Python 3+)
def working_counter():
    count = 0
    def increment():
        nonlocal count       # 告诉 Python:count 是外部的!
        count += 1
        return count
    return increment


# 方案2:用可变容器(如列表)—— 不需要 nonlocal
def counter_with_list():
    count = [0]              # 列表是可变对象
    def increment():
        count[0] += 1        # count[0] = count[0] + 1,不是赋值
        return count[0]
    return increment

# 方案3:用函数属性(hack 方式)
场景 是否需要 nonlocal
只读取外部变量 不需要
修改不可变对象(int, str, tuple) 需要 nonlocal
修改可变对象内容(list, dict) 不需要

新手易踩坑:

  • 坑 1:闭包里的 count += 1 没加 nonlocalUnboundLocalError!Python 以为你要创建局部变量
  • 坑 2:以为每次调用外部函数都会"重置"闭包 → 不会!已经返回的闭包有自己独立的变量副本
  • 坑 3:把闭包和匿名函数 lambda 搞混 → 闭包可以是 lambda,但带有外部变量

课堂小练习 2

写一个 make_averager() 函数,返回一个"求平均值"的闭包。每次调用传入一个数字,返回当前所有数字的平均值。

# 期望效果:
avg = make_averager()
print(avg(10))   # 10.0
print(avg(20))   # 15.0
print(avg(30))   # 20.0
点击查看参考答案
def make_averager():
    numbers = []             # 用可变列表,不需要 nonlocal

    def averager(new_value):
        numbers.append(new_value)
        return sum(numbers) / len(numbers)

    return averager

avg = make_averager()
print(avg(10))   # 10.0
print(avg(20))   # 15.0
print(avg(30))   # 20.0

四、装饰器入门:给函数穿上"外套"

4.1 问题的引出

假设你有一个 "打招呼" 函数,你想在它前后加上 "开始" 和 "结束" 日志:

# 原始函数
def greet(name):
    print(f"你好,{name}!")

# 想做这件事 → 在函数调用前后加日志:
# 方法1:直接修改函数(不好——如果有很多函数要改呢?)
def greet(name):
    print("[LOG] 开始执行 greet...")
    print(f"你好,{name}!")
    print("[LOG] greet 执行完毕")

# 方法2:写一个包装函数
def log_greet(name):
    print("[LOG] 开始执行 greet...")
    greet(name)
    print("[LOG] greet 执行完毕")

# 方法3:用装饰器(最美的方式!)

4.2 装饰器的本质

装饰器 = 一个接收函数作为参数、返回新函数的……函数。

# 第一步:理解装饰器的本质

def log_decorator(func):        # func 是被装饰的原始函数
    """一个最简装饰器:在函数执行前后打印日志"""
    def wrapper():
        print(f"[LOG] 即将调用 {func.__name__}...")
        func()                   # 调用原始函数
        print(f"[LOG] {func.__name__} 执行完毕")
    return wrapper               # 返回包装后的函数


# === 使用方式1:手动套装饰器 ===
def say_hello():
    print("Hello!")

say_hello = log_decorator(say_hello)  # 把 say_hello 替换成包装版
say_hello()
# 输出:
# [LOG] 即将调用 say_hello...
# Hello!
# [LOG] say_hello 执行完毕


# === 使用方式2:@ 语法糖(和第1种完全等价!)===
@log_decorator
def say_hi():
    print("Hi!")

say_hi()
# 输出:
# [LOG] 即将调用 say_hi...
# Hi!
# [LOG] say_hi 执行完毕

核心理解@log_decorator 等价于 say_hi = log_decorator(say_hi)@ 语法糖只是让代码更美观,底层逻辑完全一样。

4.3 装饰器的执行时机

def my_decorator(func):
    print(f"装饰器正在包装:{func.__name__}")  # 这行在定义时就会执行!
    def wrapper():
        print("wrapper 执行")
        func()
    return wrapper

@my_decorator
def foo():
    print("foo 执行")

print("--- 分隔线 ---")
foo()
foo()

# 输出:
# 装饰器正在包装:foo      ← 注意!这行在 @ 定义时就打印了
# --- 分隔线 ---
# wrapper 执行            ← 第一次调用
# foo 执行
# wrapper 执行            ← 第二次调用
# foo 执行

关键理解@ 那一行在定义时就执行了装饰器函数。而 wrapper 里面的代码在每次调用时才执行。

课堂小练习 3

写一个 timer 装饰器,计算被装饰函数的执行时间(用 time.time()),并打印出来。

点击查看参考答案
import time

def timer(func):
    def wrapper():
        start = time.time()
        func()
        end = time.time()
        print(f"{func.__name__} 执行耗时:{end - start:.4f} 秒")
    return wrapper

@timer
def slow_function():
    total = 0
    for i in range(10000000):
        total += i
    print(f"计算结果:{total}")

slow_function()
# 输出:
# 计算结果:49999995000000
# slow_function 执行耗时:0.3587 秒

五、被装饰函数的返回值与参数

5.1 处理返回值

之前的装饰器把原始函数的返回值弄丢了!因为 wrapper 里调用 func() 后没有 return

# 问题:装饰器吞掉了返回值
def broken_decorator(func):
    def wrapper():
        print("开始...")
        func()            # 执行了,但没 return
        print("结束...")
    return wrapper

@broken_decorator
def add():
    return 1 + 2

result = add()
print(f"结果:{result}")  # 结果:None  ← 啊?我的 3 呢?


# 修正:wrapper 里要 return
def fixed_decorator(func):
    def wrapper():
        print("开始...")
        result = func()      # 保存返回值
        print("结束...")
        return result        # 把返回值传出去!
    return wrapper

@fixed_decorator
def add():
    return 1 + 2

result = add()
print(f"结果:{result}")  # 结果:3  ← 正确!

5.2 处理任意参数(*args 和 **kwargs)

原始函数可能有各种参数,wrapper 要能通用地转发它们。

def universal_decorator(func):
    def wrapper(*args, **kwargs):          # 接收任意参数
        print(f"[LOG] 调用 {func.__name__}(*{args}, **{kwargs})")
        result = func(*args, **kwargs)     # 原样转发给原函数
        print(f"[LOG] {func.__name__} 返回 {result}")
        return result
    return wrapper


# 这个装饰器可以装饰任何函数!
@universal_decorator
def greet(name, greeting="你好"):
    return f"{greeting},{name}!"

@universal_decorator
def multiply(a, b):
    return a * b

@universal_decorator
def no_args():
    return "无参数"

print(greet("小明"))
# [LOG] 调用 greet(*('小明',), **{})
# [LOG] greet 返回 你好,小明!
# 你好,小明!

print(multiply(6, 7))
# [LOG] 调用 multiply(*(6, 7), **{})
# [LOG] multiply 返回 42
# 42

print(no_args())
# [LOG] 调用 no_args(*(), **{})
# [LOG] no_args 返回 无参数
# 无参数

5.3 万能装饰器模板

def decorator_name(func):
    """万能装饰器模板 —— 把这段背下来!"""
    import functools

    @functools.wraps(func)      # 保留原函数的元信息(见第六章)
    def wrapper(*args, **kwargs):
        # === 前置操作 ===
        # 在调用原函数之前做的事情
        print(f"即将调用 {func.__name__}...")

        # === 调用原函数 ===
        result = func(*args, **kwargs)

        # === 后置操作 ===
        # 在调用原函数之后做的事情
        print(f"{func.__name__} 执行完毕")

        # === 返回结果 ===
        return result

    return wrapper

新手易踩坑:

  • 坑 1:wrapper 里调了 func() 但没 return → 原函数的返回值丢失,变成 None
  • 坑 2:wrapper 参数写死(如 def wrapper(x, y))→ 换了别的函数就不能用了!请用 *args, **kwargs
  • 坑 3:装饰器函数本身不要写 @ 那一行的逻辑,写在 wrapper 里面写错了地方

六、functools.wraps:保留函数元信息

6.1 问题:装饰器"偷走"了函数的名字

def my_decorator(func):
    def wrapper(*args, **kwargs):
        """我是 wrapper 的文档"""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name):
    """向某人问好"""
    return f"你好,{name}!"

# 看看 greet 变成什么了?
print(greet.__name__)   # wrapper    ← 本应是 greet!
print(greet.__doc__)    # 我是 wrapper 的文档  ← 本应是"向某人问好"!
help(greet)             # 显示的是 wrapper 的帮助,完全不认识 greet 了

问题严重性:调试时看不到真正的函数名、文档丢失、某些框架(如 Flask 的路由)依赖 __name__ 属性。

6.2 解决方案:functools.wraps

from functools import wraps

def my_decorator(func):
    @wraps(func)                    # ← 就加这一行!
    def wrapper(*args, **kwargs):
        """我是 wrapper 的文档"""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name):
    """向某人问好"""
    return f"你好,{name}!"

print(greet.__name__)   # greet       ← 正确!
print(greet.__doc__)    # 向某人问好   ← 正确!
help(greet)             # 显示的是 greet 的文档

# wraps 做的事情:把 func 的 __name__, __doc__, __module__ 等
# 属性复制到 wrapper 上。它本质也是一个装饰器!

6.3 wraps 内部原理(简单版)

# functools.wraps 的简化实现(理解即可)
def simple_wraps(original_func):
    def decorator(wrapper_func):
        wrapper_func.__name__ = original_func.__name__
        wrapper_func.__doc__ = original_func.__doc__
        wrapper_func.__module__ = original_func.__module__
        wrapper_func.__dict__.update(original_func.__dict__)
        return wrapper_func
    return decorator

# 所以 @wraps(func) 等价于:
# wrapper = wraps(func)(wrapper)

[重要规则]写装饰器时,永远加上 @wraps(func),除非你有特殊原因不加。

课堂小练习 4

写一个能处理任意参数、保留元信息的装饰器 repeat(n),让被装饰函数执行 n 次,并返回最后一次的结果。

点击查看参考答案
from functools import wraps

def repeat(n):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say(message):
    print(message)
    return message.upper()

result = say("hello")
# hello
# hello
# hello
print(result)    # HELLO
print(say.__name__)  # say(元信息完整)

七、带参数的装饰器:装饰器的"配置版"

7.1 从两层到三层

装饰器如果不需要参数,是两层嵌套:

def simple_decorator(func):       # 第1层:接收函数
    def wrapper(*args, **kwargs): # 第2层:包裹逻辑
        return func(*args, **kwargs)
    return wrapper

装饰器如果需要参数,要变成三层嵌套:

def decorator_with_args(param):             # 第1层:接收装饰器参数
    def actual_decorator(func):             # 第2层:接收函数
        def wrapper(*args, **kwargs):       # 第3层:包裹逻辑
            # 这里可以使用 param
            return func(*args, **kwargs)
        return wrapper
    return actual_decorator

[记忆技巧]:带参数的装饰器就是用函数包了两层——最外层接收"配置",中间层接收函数,最内层是执行逻辑。

7.2 实战:可配置的重试装饰器

import time
from functools import wraps

def retry(max_attempts=3, delay=1):
    """
    重试装饰器
    - max_attempts:最大重试次数
    - delay:每次重试之间的等待时间(秒)
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"[重试 {attempt}/{max_attempts}] {func.__name__} 失败:{e}")
                    if attempt == max_attempts:
                        print(f"[放弃] {func.__name__} 重试 {max_attempts} 次后仍失败")
                        raise  # 最后一次重试失败,抛出异常
                    time.sleep(delay)
        return wrapper
    return decorator


# === 使用(不同的配置!) ===
import random

@retry(max_attempts=5, delay=0.5)
def unstable_network_call():
    """模拟不稳定的网络调用(60% 概率失败)"""
    if random.random() < 0.6:
        raise ConnectionError("网络超时")
    return "数据获取成功!"

@retry(max_attempts=2, delay=0.1)   # 另一个函数用不同的配置
def quick_api_call():
    if random.random() < 0.5:
        raise TimeoutError("API 超时")
    return "API 响应"

# 测试
print(unstable_network_call())

7.3 更多带参数装饰器示例

# 示例1:限速装饰器(控制函数调用频率)
from functools import wraps
import time

def rate_limit(calls_per_second):
    """限制函数每秒最多调用 calls_per_second 次"""
    min_interval = 1.0 / calls_per_second

    def decorator(func):
        last_called = [0.0]  # 用列表避免 nonlocal

        @wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - last_called[0]
            if elapsed < min_interval:
                time.sleep(min_interval - elapsed)
            result = func(*args, **kwargs)
            last_called[0] = time.time()
            return result

        return wrapper
    return decorator

@rate_limit(2)  # 每秒最多调用 2 次
def send_message(msg):
    print(f"发送:{msg}")

# 连续快速调用
for i in range(5):
    send_message(f"消息{i+1}")
# 发送:消息1  ← 立即
# (等待 0.5 秒)
# 发送:消息2
# (等待 0.5 秒)
# ...
# 示例2:权限校验装饰器
from functools import wraps

def require_role(role):
    """检查用户是否有指定角色"""
    def decorator(func):
        @wraps(func)
        def wrapper(current_user, *args, **kwargs):
            if current_user.get("role") != role:
                raise PermissionError(f"需要 {role} 角色,当前角色为 {current_user.get('role')}")
            return func(current_user, *args, **kwargs)
        return wrapper
    return decorator

admin = {"name": "管理员", "role": "admin"}
guest = {"name": "访客", "role": "guest"}

@require_role("admin")
def delete_user(user, target_id):
    return f"{user['name']} 删除了用户 {target_id}"

print(delete_user(admin, 123))  # 管理员 删除了用户 123
# print(delete_user(guest, 123)) # PermissionError!

7.4 带参数装饰器的执行流程图解

@retry(max_attempts=3, delay=1)
def my_func():
    pass

# 等价于三个步骤:
# 步骤1:retry(max_attempts=3, delay=1) → 返回 decorator
# 步骤2:decorator(my_func)            → 返回 wrapper
# 步骤3:my_func = wrapper

# 所以:
# my_func = retry(max_attempts=3, delay=1)(my_func)
#          └────────── 第1次调用 ──────────┘└─ 第2次调用 ─┘

课堂小练习 5

写一个带参数的装饰器 log_to_file(filename),把函数的调用信息(函数名、参数、返回值、时间)追加写入到指定的日志文件中。

点击查看参考答案
from functools import wraps
from datetime import datetime

def log_to_file(filename):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            start = datetime.now()
            result = func(*args, **kwargs)
            end = datetime.now()
            # 写入日志
            with open(filename, "a", encoding="utf-8") as f:
                f.write(
                    f"[{start.strftime('%Y-%m-%d %H:%M:%S')}] "
                    f"{func.__name__}(args={args}, kwargs={kwargs}) "
                    f"→ {result} "
                    f"(耗时: {(end - start).total_seconds():.4f}s)\n"
                )
            return result
        return wrapper
    return decorator

@log_to_file("app.log")
def calculate(a, b):
    return a + b

calculate(3, 5)
calculate(10, 20)
# app.log 内容:
# [2026-07-01 14:30:00] calculate(args=(3, 5), kwargs={}) → 8 (耗时: 0.0001s)
# [2026-07-01 14:30:01] calculate(args=(10, 20), kwargs={}) → 30 (耗时: 0.0001s)

八、多层装饰器:叠罗汉

8.1 多个装饰器的堆叠

一个函数可以套多个装饰器,像穿衣服一样:先贴身的先穿,最外层的后穿。

from functools import wraps

def decorator_a(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("[A 外层] 进入")
        result = func(*args, **kwargs)
        print("[A 外层] 退出")
        return result
    return wrapper

def decorator_b(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("[B 中层] 进入")
        result = func(*args, **kwargs)
        print("[B 中层] 退出")
        return result
    return wrapper

def decorator_c(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("[C 内层] 进入")
        result = func(*args, **kwargs)
        print("[C 内层] 退出")
        return result
    return wrapper


@decorator_a     # 最外层,最后执行
@decorator_b     # 中层
@decorator_c     # 最内层,最先执行
def hello():
    print(">>> 原始函数执行 <<<")

hello()

# 输出:
# [A 外层] 进入       ← 洋葱的最外层先进入
# [B 中层] 进入
# [C 内层] 进入
# >>> 原始函数执行 <<<  ← 洋葱芯
# [C 内层] 退去
# [B 中层] 退去
# [A 外层] 退去

8.2 洋葱模型

调用 → ┌─── A ────────────────────┐
       │  ┌─── B ────────────┐    │
       │  │  ┌─── C ────┐    │    │
       │  │  │  原始函数  │    │    │
       │  │  └──────────┘    │    │
       │  └─────────────────┘    │
       └────────────────────────┘ → 返回

执行顺序:A 外 → B 外 → C 外 → 原始 → C 内 → B 内 → A 内
就像剥洋葱:从外到内,再从内到外

8.3 等价代码

# @decorator_a
# @decorator_b
# @decorator_c
# def hello():
#     pass

# 完全等价于:
hello = decorator_a(decorator_b(decorator_c(hello)))
#                  └────── 先执行 ──────┘
#         └─────────────── 最后执行 ───────────────┘

课堂小练习 6

写两个装饰器:uppercase_result(把返回值转大写)和 add_exclamation(在返回值后加感叹号),然后把它们同时用在 greet 函数上。思考:执行顺序如何影响最终结果?

点击查看参考答案
from functools import wraps

def add_exclamation(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) + "!!!"
    return wrapper

def uppercase_result(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

# 顺序1:
@uppercase_result
@add_exclamation
def greet1(name):
    return f"hello {name}"

print(greet1("Tom"))  # HELLO TOM!!!
# 流程:greet1("Tom") → "hello Tom" → add_exclamation → "hello Tom!!!" → uppercase → "HELLO TOM!!!"

# 顺序2:
@add_exclamation
@uppercase_result
def greet2(name):
    return f"hello {name}"

print(greet2("Tom"))  # HELLO TOM!!!
# 流程:greet2("Tom") → "hello Tom" → uppercase → "HELLO TOM" → add_exclamation → "HELLO TOM!!!"

九、类装饰器:用类实现的装饰器

9.1 利用 call 方法

除了函数,也可以做装饰器。只要类实现了 __call__ 方法,其实例就可以像函数一样被调用。

from functools import wraps

class CountCalls:
    """统计函数被调用次数的装饰器"""

    def __init__(self, func):
        self.func = func       # 保存原始函数
        self.count = 0         # 初始化计数器

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"[计数] {self.func.__name__} 已被调用 {self.count} 次")
        return self.func(*args, **kwargs)


@CountCalls    # 等价于 my_func = CountCalls(my_func)
def my_func():
    print("执行 my_func")

my_func()  # [计数] my_func 已被调用 1 次 → 执行 my_func
my_func()  # [计数] my_func 已被调用 2 次 → 执行 my_func
my_func()  # [计数] my_func 已被调用 3 次 → 执行 my_func

9.2 带参数的类装饰器

from functools import wraps

class Retry:
    """带参数的重试装饰器(类版本)"""

    def __init__(self, max_attempts=3, delay=1):
        self.max_attempts = max_attempts
        self.delay = delay

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            import time
            for attempt in range(1, self.max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"[重试 {attempt}/{self.max_attempts}] {e}")
                    if attempt == self.max_attempts:
                        raise
                    time.sleep(self.delay)
        return wrapper


@Retry(max_attempts=4, delay=0.5)  # 注意:Retry 是类,但用法一模一样!
def risky_operation():
    import random
    if random.random() < 0.7:
        raise ValueError("操作失败")
    return "成功!"

9.3 函数装饰器 vs 类装饰器

特性 函数装饰器 类装饰器
写法 两层/三层嵌套函数 类 +__call__
状态保持 靠闭包/nonlocal 靠实例变量 self.xxx
可读性 简单场景更直白 复杂状态管理更清晰
扩展性 难以继承 可以继承和复用

建议:简单装饰器用函数,需要复杂状态管理用类。


十、内置装饰器:@staticmethod / @classmethod / @property

Python 自带三个常用装饰器,用于类的内部方法。

10.1 @staticmethod:静态方法

不需要访问实例(self)或类(cls)的方法。相当于放在类里的普通函数。

class MathUtils:
    @staticmethod
    def add(a, b):
        """静态方法:和普通函数一样,但逻辑上归属于这个类"""
        return a + b

    @staticmethod
    def is_even(n):
        return n % 2 == 0

# 可以直接通过类调用,不需要创建实例
print(MathUtils.add(3, 5))    # 8
print(MathUtils.is_even(10))  # True

10.2 @classmethod:类方法

第一个参数是类本身cls),可以访问类属性。常用于工厂方法(用不同方式创建实例)。

class Student:
    school = "第一中学"

    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_birth_year(cls, name, birth_year):
        """工厂方法:从出生年份创建学生"""
        age = 2026 - birth_year
        return cls(name, age)       # cls = Student

    @classmethod
    def change_school(cls, new_school):
        """类方法可以修改类属性"""
        cls.school = new_school

# 正常创建
s1 = Student("小明", 18)

# 用工厂方法创建
s2 = Student.from_birth_year("小红", 2008)  # 小红,18 岁
print(s2.name, s2.age)  # 小红 18

# 修改类属性
Student.change_school("第二中学")
print(Student.school)   # 第二中学

10.3 @property:把方法变成属性

让方法可以像属性一样访问(不加括号),常用于计算属性属性校验

class Circle:
    def __init__(self, radius):
        self._radius = radius   # 用 _ 表示"私有"

    @property
    def radius(self):
        """获取半径 —— 像属性一样访问"""
        return self._radius

    @radius.setter
    def radius(self, value):
        """设置半径 —— 可以加校验逻辑!"""
        if value <= 0:
            raise ValueError("半径必须为正数")
        self._radius = value

    @property
    def area(self):
        """只读属性:面积(不需存,每次计算)"""
        import math
        return math.pi * self._radius ** 2

    @property
    def diameter(self):
        """只读属性:直径"""
        return self._radius * 2


c = Circle(5)
print(c.radius)     # 5        ← 不用写 c.radius()
print(c.area)       # 78.54    ← 像属性一样访问,但实际在计算
print(c.diameter)   # 10

c.radius = 10       # 触发 setter
print(c.area)       # 314.16

# c.radius = -1     # ValueError: 半径必须为正数

三种内置装饰器对比:

装饰器 第一个参数 是否访问实例 是否访问类 典型场景
无装饰器(普通方法) self 大部分方法
@staticmethod 无特殊参数 工具函数、不依赖实例的逻辑
@classmethod cls 工厂方法、修改类属性
@property self 计算属性、属性校验

十一、综合实战案例

案例 1:函数执行时间统计器(完整版)

import time
from functools import wraps
from collections import defaultdict

# 全局统计表,记录每个函数的调用次数和总耗时
stats = defaultdict(lambda: {"calls": 0, "total_time": 0.0})

def profile(func):
    """统计函数的调用次数和总耗时"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            elapsed = time.perf_counter() - start
            stats[func.__name__]["calls"] += 1
            stats[func.__name__]["total_time"] += elapsed
    return wrapper

def print_stats():
    """打印所有被 profile 装饰的函数的统计信息"""
    print(f"\n{'函数名':<20} {'调用次数':<10} {'总耗时(s)':<12} {'平均耗时(s)'}")
    print("-" * 60)
    for name, data in sorted(stats.items()):
        avg = data["total_time"] / data["calls"]
        print(f"{name:<20} {data['calls']:<10} {data['total_time']:<12.4f} {avg:.6f}")

# 使用
@profile
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

@profile
def slow_task(n):
    total = 0
    for i in range(n):
        total += i
    return total

fibonacci(30)
slow_task(5000000)
print_stats()

案例 2:缓存装饰器(手写简易版 functools.lru_cache)

from functools import wraps

def memoize(func):
    """缓存函数调用结果——相同的输入参数直接返回缓存结果"""
    cache = {}

    @wraps(func)
    def wrapper(*args):
        if args in cache:
            print(f"[缓存命中] {func.__name__}{args} → {cache[args]}")
            return cache[args]
        result = func(*args)
        cache[args] = result
        print(f"[缓存新增] {func.__name__}{args} → {result}")
        return result

    return wrapper


@memoize
def fib(n):
    """计算第 n 个斐波那契数"""
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

print("\n=== 第一次计算 fib(10) ===")
result = fib(10)
print(f"结果:{result}")

print("\n=== 第二次计算 fib(10) ===")
result = fib(10)  # 这次直接走缓存
print(f"结果:{result}")

# 输出中可以看到大量 [缓存命中],避免了重复计算

案例 3:Flask 风格路由装饰器(理解 Web 框架原理)

from functools import wraps

class MiniWebApp:
    """极简 Web 框架——演示装饰器在 Web 中的应用"""
    def __init__(self):
        self.routes = {}

    def route(self, path):
        """路由装饰器:把函数注册到指定路径"""
        def decorator(func):
            self.routes[path] = func
            @wraps(func)
            def wrapper(*args, **kwargs):
                return func(*args, **kwargs)
            return wrapper
        return decorator

    def handle_request(self, path):
        """模拟处理 HTTP 请求"""
        if path in self.routes:
            return self.routes[path]()
        return "404 Not Found"


app = MiniWebApp()

@app.route("/")
def home():
    return "首页 - 欢迎访问!"

@app.route("/about")
def about():
    return "关于我们"

@app.route("/contact")
def contact():
    return "联系我们"

# 模拟请求
print(app.handle_request("/"))         # 首页 - 欢迎访问!
print(app.handle_request("/about"))    # 关于我们
print(app.handle_request("/unknown"))  # 404 Not Found

案例 4:类型校验装饰器

from functools import wraps

def validate_types(**expected_types):
    """校验函数参数的类型"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 校验位置参数(简化版:按参数名校验)
            import inspect
            sig = inspect.signature(func)
            bound = sig.bind(*args, **kwargs)
            bound.apply_defaults()

            for name, value in bound.arguments.items():
                if name in expected_types:
                    expected = expected_types[name]
                    if not isinstance(value, expected):
                        raise TypeError(
                            f"参数 '{name}' 期望 {expected.__name__},"
                            f"实际得到 {type(value).__name__}"
                        )
            return func(*args, **kwargs)
        return wrapper
    return decorator


@validate_types(name=str, age=int, score=float)
def add_student(name, age, score=0.0):
    return f"学生 {name},年龄 {age},成绩 {score}"

print(add_student("小明", 18, 95.5))  # 学生 小明,年龄 18,成绩 95.5
# print(add_student("小明", "十八", 95.5))  # TypeError: 参数 'age' 期望 int,实际得到 str

十二、常见面试题与易错点

12.1 易错点汇总

错误 1:装饰器没写 return 导致返回值丢失

def log(func):
    def wrapper(*args, **kwargs):
        print("调用前...")
        func(*args, **kwargs)        # 这里没 return!
        print("调用后...")
    return wrapper

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

print(add(1, 2))  # None ← 啊?3 去哪了?
# 修正:result = func(*args, **kwargs); return result

错误 2:忘了加 @wraps(func)

# 后果:函数名变成 wrapper,文档丢失,某些依赖 __name__ 的库(如 Flask)出 bug
# 解决:永远加上 @wraps(func)

错误 3:把带参数的装饰器写成两层

# 想要:@repeat(3)
# 错误写法(两层):
def repeat(n):
    def wrapper(func):          # ← 错了!wrapper 要接收的是 func
        for i in range(n):
            func()              # ← 但这里直接执行了!不是在 wrapper 里
        return func
    return wrapper

# 正确写法(三层):
def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

错误 4:装饰器在 @ 定义时就执行,不是调用时

def register(func):
    print(f"注册了:{func.__name__}")  # 定义时就打印!
    return func

@register
def foo():
    pass
# 输出:"注册了:foo" ← 还没调用 foo 呢!

错误 5:多层装饰器的顺序搞反

@decorator_a     # 后执行(外层)
@decorator_b     # 先执行(内层)
def func():
    pass

# 等价于:func = decorator_a(decorator_b(func))
# decorator_b 先生效(离函数近),decorator_a 后生效

12.2 经典面试题

题 1:以下代码输出什么?

def decorator(func):
    def wrapper(*args, **kwargs):
        print("A")
        result = func(*args, **kwargs)
        print("B")
        return result
    return wrapper

@decorator
def foo():
    print("C")

foo()
答案
A
C
B

解析:装饰器的 wrapper 在原始函数前后加了打印。顺序:wrapper 前 → 原始函数 → wrapper 后。


题 2:以下代码输出什么?(经典陷阱!)

def outer():
    funcs = []
    for i in range(3):
        def inner():
            return i
        funcs.append(inner)
    return funcs

for f in outer():
    print(f(), end=" ")
答案
2 2 2

不是 0 1 2!原因是:inner 中的 i 引用了同一个外部变量。当 inner 最终被调用时,i 已经是 2 了。

修正方法(用默认参数"快照"绑定的值):

def outer():
    funcs = []
    for i in range(3):
        def inner(i=i):     # 用默认参数绑定当前值
            return i
        funcs.append(inner)
    return funcs

for f in outer():
    print(f(), end=" ")  # 0 1 2

题 3:手写一个 @singleton 装饰器,确保被装饰的类只有一个实例。

答案
from functools import wraps

def singleton(cls):
    """单例模式装饰器"""
    instances = {}

    @wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return get_instance

@singleton
class Config:
    def __init__(self):
        print("Config 初始化(只会执行一次)")
        self.settings = {}

c1 = Config()
c2 = Config()
print(c1 is c2)  # True——同一个实例!

题 4:装饰器和闭包的关系是什么?

答案

装饰器本质上是闭包的一种应用。装饰器 = 一个接收函数作为参数、返回新函数(闭包)的函数。

具体来说:

  • 闭包是"内部函数引用外部变量"
  • 装饰器利用闭包来"包裹"原始函数:wrapper(闭包)记住了 func(外部变量),在调用 wrapper 时仍然能访问 func
def decorator(func):
    def wrapper():       # wrapper 是闭包
        return func()    # func 是自由变量(被闭包记住的)
    return wrapper

总结

装饰器知识地图:

                        函数是一等公民
                        (函数可传参、可返回)
                              │
                    闭包(函数携带记忆)
                    (内部函数引用外部变量)
                              │
                    ┌─────────┴─────────┐
                    ↓                   ↓
              无参装饰器            带参装饰器
            (def + @语法)       (三层嵌套函数)
                    │                   │
                    └────────┬──────────┘
                             ↓
                      @functools.wraps
                    (保留原函数元信息)
                             │
              ┌──────────────┼──────────────┐
              ↓              ↓              ↓
         多层装饰器      类装饰器        内置装饰器
       (洋葱模型)    (__call__)   (@staticmethod
                                       @classmethod
                                       @property)

核心心法三句话:

  1. 闭包 = 内部函数"记住"了外部函数的变量
  2. 装饰器 = 用闭包给函数套上"外套",加功能不改代码
  3. 永远加 @wraps(func),保留函数名和文档
0
博主关闭了当前页面的评论