Day38 - 装饰器详解 (@decorator/wraps/闭包)

详细讲解

1. 闭包基础

1.1 什么是闭包

闭包是指一个函数记住其创建时所处的环境(外部作用域的变量)的机制。

def outer():
    x = 10  # 外部变量
    
    def inner():
        print(x)  # 引用外部变量
    
    return inner

# 创建闭包
closure = outer()
closure()  # 输出 10

# 验证闭包
print(f"closure.__closure__: {closure.__closure__}")  # 闭包变量
print(f"closure.__code__.co_freevars: {closure.__code__.co_freevars}")  # 自由变量名

1.2 闭包的经典应用

def make_multiplier(factor):
    """创建乘数函数(闭包)"""
    def multiply(x):
        return x * factor
    return multiply

# 创建两个不同的乘数
times_2 = make_multiplier(2)
times_3 = make_multiplier(3)
times_5 = make_multiplier(5)

print(times_2(10))  # 20
print(times_3(10))  # 30
print(times_5(10))  # 50

# 闭包会记住创建时的 factor 值
print(f"times_2 的闭包: {times_2.__closure__[0].cell_contents}")  # 2

1.3 闭包的常见陷阱

# 陷阱:循环中的闭包
def create_funcs():
    funcs = []
    for i in range(3):
        # 错误!所有函数都会引用同一个 i
        def func():
            return i
        funcs.append(func)
    return funcs

funcs = create_funcs()
print([f() for f in funcs])  # [2, 2, 2] - 不是 [0, 1, 2]!

# 正确做法:使用默认参数捕获当前值
def create_funcs_fixed():
    funcs = []
    for i in range(3):
        def func(i=i):  # 默认参数在定义时绑定
            return i
        funcs.append(func)
    return funcs

funcs = create_funcs_fixed()
print([f() for f in funcs])  # [0, 1, 2]

2. 装饰器基础

装饰器是一个返回函数的高阶函数,用于在不修改原函数的情况下扩展功能。

2.1 简单装饰器

def my_decorator(func):
    """装饰器函数"""
    def wrapper(*args, **kwargs):
        print("函数开始执行")
        result = func(*args, **kwargs)
        print("函数执行结束")
        return result
    return wrapper

@my_decorator
def say_hello(name):
    print(f"你好,{name}!")

# 等价于
say_hello = my_decorator(say_hello)

say_hello("张三")

2.2 装饰器的执行时机

print("装饰器定义位置")

@my_decorator  # 装饰器在被装饰函数定义时立即执行
def test():
    print("test 函数执行")

print("装饰器定义后")

# 输出顺序:
# 装饰器定义位置
# 装饰器定义后
# (装饰器函数执行时输出 "函数开始执行")
# test 函数执行
# 函数执行结束

2.3 带参数的装饰器

def repeat(times):
    """带参数的装饰器工厂函数"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)  # 会打印 3 次
def greet(name):
    print(f"你好,{name}!")

greet("李四")
# 输出:
# 你好,李四!
# 你好,李四!
# 你好,李四!

3. functools.wraps 详解

wraps 用于保留原函数的元信息(名称、文档等)。

import functools

def my_decorator(func):
    @functools.wraps(func)  # 保留原函数元信息
    def wrapper(*args, **kwargs):
        print("扩展功能")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def original_func():
    """这是 original_func 的文档"""
    pass

# 不使用 wraps 的问题
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    wrapper.__name__ = func.__name__  # 手动复制(不推荐)
    return wrapper

# 使用 wraps
print(original_func.__name__)  # 'original_func'
print(original_func.__doc__)   # '这是 original_func 的文档'

4. 类装饰器

import functools

class CountCalls:
    """计数装饰器类"""
    def __init__(self, func):
        self.func = func
        self.count = 0
        functools.update_wrapper(self, func)
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"函数被调用了 {self.count} 次")
        return self.func(*args, **kwargs)

@CountCalls
def some_function():
    return 42

some_function()  # 函数被调用了 1 次
some_function()  # 函数被调用了 2 次
print(some_function.count)  # 2

5. 多重装饰器

装饰器可以叠加,按从下到上的顺序执行。

def decorator1(func):
    def wrapper(*args, **kwargs):
        print("装饰器 1 开始")
        result = func(*args, **kwargs)
        print("装饰器 1 结束")
        return result
    return wrapper

def decorator2(func):
    def wrapper(*args, **kwargs):
        print("装饰器 2 开始")
        result = func(*args, **kwargs)
        print("装饰器 2 结束")
        return result
    return wrapper

@decorator1
@decorator2
def hello():
    print("Hello!")

# 执行顺序:
# 装饰器 1 开始
# 装饰器 2 开始
# Hello!
# 装饰器 2 结束
# 装饰器 1 结束

6. 装饰器实际应用

6.1 计时器装饰器

import functools
import time

def timer(func):
    """函数执行计时装饰器"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} 执行耗时: {end - start:.4f} 秒")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "完成"

@timer
def calculate_sum(n):
    return sum(range(n))

6.2 日志装饰器

import functools
import logging

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def log_calls(func):
    """日志记录装饰器"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logger.info(f"调用 {func.__name__}, 参数: args={args}, kwargs={kwargs}")
        try:
            result = func(*args, **kwargs)
            logger.info(f"{func.__name__} 返回: {result}")
            return result
        except Exception as e:
            logger.error(f"{func.__name__} 抛出异常: {e}")
            raise
    return wrapper

@log_calls
def divide(a, b):
    return a / b

6.3 缓存装饰器

import functools

def memoize(func):
    """简单的缓存装饰器(适用于纯函数)"""
    cache = {}
    
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    
    # 暴露缓存接口
    wrapper.cache = cache
    wrapper.clear_cache = lambda: cache.clear()
    return wrapper

@memoize
def fibonacci(n):
    """斐波那契数列(带缓存)"""
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 使用缓存
print(fibonacci(100))  # 很快,因为使用了缓存
print(fibonacci.cache)  # 查看缓存内容

6.4 重试装饰器

import functools
import time

def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
    """重试装饰器"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    attempts += 1
                    if attempts >= max_attempts:
                        raise
                    print(f"第 {attempts} 次尝试失败,重试中... ({e})")
                    time.sleep(delay)
            return wrapper
        return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_api_call():
    import random
    if random.random() < 0.7:
        raise ConnectionError("连接失败")
    return "成功"

6.5 类型检查装饰器

import functools

def type_check(func):
    """运行时类型检查装饰器"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 获取函数签名
        hints = func.__annotations__
        
        # 检查参数类型
        for arg, (name, expected_type) in zip(args, list(hints.items())[:-1]):
            if name in hints and not isinstance(arg, hints[name]):
                raise TypeError(f"参数 {name} 期望 {expected_type.__name__}, 得到 {type(arg).__name__}")
        
        return func(*args, **kwargs)
    return wrapper

@type_check
def add(a: int, b: int) -> int:
    return a + b

# add(1, 2)  # 3
# add("1", "2")  # TypeError

7. 装饰器类与装饰器工厂

import functools

# 装饰器工厂:返回装饰器的函数
def requires_permission(permission):
    """权限检查装饰器工厂"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 假设有一个权限检查逻辑
            if check_permission(permission):
                return func(*args, **kwargs)
            else:
                raise PermissionError(f"需要 {permission} 权限")
        return wrapper
    return decorator

def check_permission(permission):
    """模拟权限检查"""
    return True

@requires_permission('admin')
def delete_user(user_id):
    print(f"删除用户 {user_id}")

# 使用类作为装饰器
class RateLimit:
    """速率限制装饰器类"""
    def __init__(self, max_calls=10, period=60):
        self.max_calls = max_calls
        self.period = period
        self.calls = []
    
    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            self.calls = [t for t in self.calls if now - t < self.period]
            
            if len(self.calls) >= self.max_calls:
                raise RuntimeError("速率限制触发")
            
            self.calls.append(now)
            return func(*args, **kwargs)
        return wrapper

@RateLimit(max_calls=5, period=10)
def api_endpoint():
    return "API 响应"

8. 装饰器与类方法

import functools

def log_method(func):
    """类方法装饰器"""
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        print(f"调用方法 {func.__name__}")
        return func(self, *args, **kwargs)
    return wrapper

def log_classmethods(cls):
    """类装饰器:自动装饰所有方法"""
    for name, method in vars(cls).items():
        if callable(method) and not name.startswith('_'):
            setattr(cls, name, log_method(method))
    return cls

@log_classmethods
class MyClass:
    def method1(self):
        return "方法1"
    
    def method2(self):
        return "方法2"

obj = MyClass()
obj.method1()  # 调用方法 method1
obj.method2()  # 调用方法 method2

背诵版

核心速查

┌─────────────────────────────────────────────────────────────┐ │ 装饰器速查 │ ├─────────────────────────────────────────────────────────────┤ │ @decorator ─ 应用装饰器 │ │ @decorator(args) ─ 带参数装饰器 │ │ functools.wraps ─ 保留原函数元信息 │ │ 多重装饰器 ─ 从下到上的顺序执行 │ └─────────────────────────────────────────────────────────────┘

装饰器模板

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 前置处理
        result = func(*args, **kwargs)
        # 后置处理
        return result
    return wrapper

# 带参数的装饰器
def decorator_factory(arg):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 使用 arg
            return func(*args, **kwargs)
        return wrapper
    return decorator

考前记忆

面试重点

  1. 装饰器的本质

    • 本质是一个返回函数的高阶函数
    • 用于不修改原函数的情况下扩展功能
  2. @functools.wraps 的作用

    • 保留原函数的 __name____doc__ 等元信息
    • 必须在 wrapper 函数上使用
  3. 装饰器的执行顺序

    • 多重装饰器从下到上执行
    • 装饰发生在函数定义时,不是在调用时
  4. 闭包与装饰器的关系

    • 装饰器利用闭包来"记住"被装饰的函数
    • wrapper 闭包引用了原函数
  5. *args, **kwargs 的重要性

    • 让装饰器通用化,传递任意参数
    • 使用 functools.wraps 保留签名信息

记忆口诀

装饰器是返回函数的高阶函数, wraps 保留元信息。 闭包记住外部变量, 装饰顺序从下到上。

测试题

选择题

1. 装饰器的本质是什么?

# A. 一个类
# B. 一个返回函数的高阶函数
# C. 一个特殊语法
# D. 一个闭包

答案:B


2. 以下代码的输出是什么?

def decorator(func):
    def wrapper():
        print("A")
        func()
    return wrapper

@decorator
def say():
    print("B")

say()
# A. A
# B. B
# C. A B
# D. B A

答案:C


3. functools.wraps 的作用是?

# A. 创建装饰器
# B. 保留原函数的元信息
# C. 执行函数
# D. 创建闭包

答案:B


4. 多重装饰器的执行顺序是?

@decorator1
@decorator2
def func():
    pass

# A. 先执行 decorator1,再执行 decorator2
# B. 先执行 decorator2,再执行 decorator1
# C. 同时执行
# D. 不执行任何装饰器

答案:B(从下到上)


5. 闭包的作用是?

# A. 加快函数执行
# B. 让函数记住外部作用域的变量
# C. 限制函数作用域
# D. 创建类

答案:B


编程题

1. 实现一个日志装饰器:

import functools
import time

def log(func):
    """日志记录装饰器"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 调用 {func.__name__}")
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {func.__name__} 返回 {result}, 耗时 {end-start:.4f}s")
        return result
    return wrapper

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

@log
def slow_func():
    time.sleep(1)
    return "完成"

2. 实现一个缓存装饰器:

import functools
import hashlib
import json

def cache(func):
    """缓存装饰器"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 生成缓存键
        key = f"{func.__name__}_{args}_{kwargs}"
        cache_key = hashlib.md5(str(key).encode()).hexdigest()
        
        # 检查缓存
        if hasattr(wrapper, '_cache') and cache_key in wrapper._cache:
            print(f"缓存命中: {cache_key}")
            return wrapper._cache[cache_key]
        
        # 执行函数
        result = func(*args, **kwargs)
        
        # 保存到缓存
        if not hasattr(wrapper, '_cache'):
            wrapper._cache = {}
        wrapper._cache[cache_key] = result
        
        return result
    return wrapper

@cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(100))  # 第一次计算
print(fibonacci(100))  # 缓存命中

3. 实现一个类装饰器:

import functools

class Validator:
    """验证器装饰器类"""
    def __init__(self, func):
        self.func = func
        functools.update_wrapper(self, func)
    
    def __call__(self, *args, **kwargs):
        # 验证逻辑
        if hasattr(self.func, '__annotations__'):
            hints = self.func.__annotations__
            for arg, (name, expected) in zip(args, list(hints.items())):
                if name in hints and not isinstance(arg, expected):
                    raise TypeError(f"{name} 应该是 {expected.__name__}")
        return self.func(*args, **kwargs)

@Validator
def add(a: int, b: int) -> int:
    return a + b

问答题

Q1: 什么是装饰器?请解释装饰器的工作原理。

装饰器是一个接受函数作为参数并返回新函数的函数。其工作原理:

  1. 当 Python 遇到 @decorator 时,会将被装饰的函数作为参数传给装饰器函数
  2. 装饰器函数返回一个新的包装函数(wrapper)
  3. 原来的函数名被替换为包装函数
  4. 调用原函数时,实际执行的是包装函数
@decorator
def func():
    pass

# 等价于
func = decorator(func)

Q2: 为什么需要 functools.wraps?如果不使用会有什么后果?

functools.wraps 用于将原函数的元信息复制到包装函数中。如果不使用:

  • func.__name__ 会变成 'wrapper'
  • func.__doc__ 会变成包装函数的文档或为空
  • 可能影响调试和反射工具

使用 wraps 可以保留原函数的名称、文档、模块等信息。


Q3: 装饰器和闭包有什么区别和联系?

区别

  • 闭包是一种语言特性,指函数记住外部作用域变量
  • 装饰器是一种应用闭包的设计模式

联系

  • 装饰器的 wrapper 函数就是一个闭包
  • 它引用了被装饰的原函数(外部变量)
def decorator(func):  # func 是外部变量
    def wrapper(*args):  # wrapper 是闭包
        return func(*args)  # 引用外部变量 func
    return wrapper

参考资料