11 装饰器(⚠️ 人工关注)
📋 资料说明
- 来源:课程 day10 后半
- 笔记:
尚硅谷大模型技术之Python1.0.docx第 12 章 - 每日一考:
day10_每日一考.md - 视频:❌ 跳过(共 8 个视频:装饰器介绍 → 闭包实现 → 语法糖 → 多层装饰器 → 带参数 → 类装饰器)
🟢 🤖 AI 替代率:82%
为什么是这个水平:
装饰器的语法(@decorator)AI 可以完美生成。但以下核心难点需要人工理解:
- 装饰器 = 闭包 + 函数对象:装饰器本质是一个接受函数、返回函数的函数。不理解闭包就不理解装饰器。
- 多层装饰器的执行顺序:这是最大的认知陷阱。装饰顺序是"离函数近的先装饰",而执行顺序相反——"离函数近的后执行"。违反直觉。
- functools.wraps:被装饰后函数的
__name__和__doc__会被替换,需要用@wraps保留元数据。这不是语法规则,而是工程实践中的"坑"。
👤 人工干预率:18% | 人类重点关注
这是本模块最高人工干预率的章节。 核心原因:
- 函数是 Python 的一等公民:装饰器依赖这个前提。如果你来自 Java,需要建立"函数本身也是对象、可以传来传去"的心智模型。
- 多层装饰器执行流程:装饰是从下往上,执行是从上往下。很多人需要画出流程图才能理解。
- 带参数装饰器的两层嵌套:
@decorator(args)实际上是@decorator(args)→decorator(args)返回一个装饰器 → 这个装饰器再装饰函数。两层抽象。
人类与 AI 协作建议:
- 让 AI 生成所有装饰器的示例代码
- 手动执行下一节的"多层装饰器流程图",在纸上画出装饰顺序和调用顺序
- 任何
@符号在代码中出现时,想想它背后发生了什么 - 用
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), {})
# 返回: 8functools.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(靠近函数先装饰)