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
考前记忆
面试重点
-
装饰器的本质
- 本质是一个返回函数的高阶函数
- 用于不修改原函数的情况下扩展功能
-
@functools.wraps的作用- 保留原函数的
__name__、__doc__等元信息 - 必须在 wrapper 函数上使用
- 保留原函数的
-
装饰器的执行顺序
- 多重装饰器从下到上执行
- 装饰发生在函数定义时,不是在调用时
-
闭包与装饰器的关系
- 装饰器利用闭包来"记住"被装饰的函数
- wrapper 闭包引用了原函数
-
*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: 什么是装饰器?请解释装饰器的工作原理。
装饰器是一个接受函数作为参数并返回新函数的函数。其工作原理:
- 当 Python 遇到
@decorator时,会将被装饰的函数作为参数传给装饰器函数 - 装饰器函数返回一个新的包装函数(wrapper)
- 原来的函数名被替换为包装函数
- 调用原函数时,实际执行的是包装函数
@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