Python(三十六) 装饰器与闭包完整教程
目录
- 开篇:从"手机壳"理解装饰器
- 前置知识:函数是一等公民
- 闭包:函数携带记忆
- 装饰器入门:给函数穿上"外套"
- 被装饰函数的返回值与参数
- functools.wraps:保留函数元信息
- 带参数的装饰器:装饰器的"配置版"
- 多层装饰器:叠罗汉
- 类装饰器:用类实现的装饰器
- 内置装饰器:@staticmethod / @classmethod / @property
- 综合实战案例
- 常见面试题与易错点
一、开篇:从"手机壳"理解装饰器
1.1 生活类比
你去买了一部新手机(原始函数),为了防摔给它套了个手机壳(装饰器)。
- 手机本身(原始函数):能打电话、发短信——功能完全没变
- 套上手机壳(装饰器):手机还是那个手机,但多了防摔保护,还不影响打电话
更重要的是:朋友的手机也能套同款手机壳——装饰器可以重复使用!
# 类比到代码:
原函数 = 裸机
@装饰器
原函数 = 套了壳的手机 → 功能不变,多了附加能力
1.2 本教程学习路线
函数是一等公民(函数可以当参数传、当返回值返回)
│
└── 闭包(内层函数记住了外层函数的变量)
│
└── 装饰器(闭包的典型应用:给函数加功能)
│
├── 基础装饰器(无参数)
│
├── functools.wraps(保留函数名和文档)
│
├── 带参数的装饰器(三层嵌套)
│
├── 多层装饰器(多个装饰器叠加)
│
└── 类装饰器(用 __call__ 实现)
二、前置知识:函数是一等公民
在 Python 中,函数是"一等公民"。这意味着函数可以:
- 赋值给变量
- 作为参数传给另一个函数
- 作为返回值从函数中返回
- 存储在数据结构(列表、字典)中
# 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),它把 func 在 value 上连续应用两次。例如 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 闭包的条件
一个函数要成为闭包,必须满足三个条件:
- 嵌套函数:内部函数定义在外部函数里面
- 引用外部变量:内部函数使用了外部函数的变量(称为"自由变量")
- 返回内部函数:外部函数把内部函数作为返回值返回
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没加nonlocal→UnboundLocalError!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)
核心心法三句话:
- 闭包 = 内部函数"记住"了外部函数的变量
- 装饰器 = 用闭包给函数套上"外套",加功能不改代码
- 永远加
@wraps(func),保留函数名和文档