第二十三篇:装饰器与偏函数——代码复用的利器
前言
**装饰器(Decorator)和偏函数(partial)**是 Python 中实现代码复用和增强功能的两大核心机制。本文深入讲解装饰器的原理、常见模式以及偏函数的应用场景。
装饰器基础
什么是装饰器?
装饰器 = 接受函数作为参数并返回新函数的函数。核心作用是在不修改原函数源码的情况下增强其行为。
def my_decorator(func):
"""装饰器:在调用前后添加日志"""
def wrapper(*args, **kwargs):
print(f"[前置] 调用 {func.__name__}")
result = func(*args, **kwargs)
print(f"[后置] 得到结果:{result}")
return result
return wrapper
# 使用 @ 语法糖
@my_decorator
def add(a, b):
return a + b
print(add(3, 5))
# [前置] 调用 add
# [后置] 得到结果:8
# 输出:8
装饰器的内部原理
# 以下两种方式等价:
# 方式一:使用 @ 语法糖
@my_decorator
def greet(name):
return f"Hello, {name}!"
# 方式二:手动赋值
def greet(name):
return f"Hello, {name}!"
greet = my_decorator(greet)
print(greet("Alice")) # Hello, Alice!
functools.wraps — 保留元信息
import functools
def logging_decorator(func):
@functools.wraps(func) # ← 关键:保留 __name__, __doc__ 等元数据
def wrapper(*args, **kwargs):
"""Wrapper 的 docstring"""
print(f"Calling {func.__name__}...")
return func(*args, **kwargs)
wrapper.__original_doc__ = func.__doc__ # 额外保留原文档
return wrapper
@logging_decorator
def calculate(x, y):
"""计算两个数之和"""
return x + y
print(calculate.__name__) # calculate — 不是 wrapper!
print(calculate.__doc__) # 计算两个数之和
实战装饰器模式
计时器装饰器
import time
import functools
def timer(func):
"""测量函数执行时间"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
elapsed = end - start
# 根据耗时选择不同的提示语
if elapsed < 0.001:
unit, display = "μs", int(elapsed * 1_000_000)
elif elapsed < 1:
unit, display = "ms", int(elapsed * 1_000)
else:
unit, display = "s", round(elapsed, 2)
print(f"{func.__name__:20s} [{unit:2s}] {display:>8}")
return result
wrapper.elapsed_times = [] # 记录历史耗时
return wrapper
@timer
def sort_list(n):
import random
data = [random.randint(0, 1000) for _ in range(n)]
return sorted(data)
@timer
def find_max(n):
import random
data = [random.randint(0, 1000) for _ in range(n)]
return max(data)
sort_list(10000)
find_max(10000)
重试装饰器
import functools
import time
def retry(max_retries=3, delay=1, backoff=2):
"""自动重试装饰器,支持指数退避"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
current_delay = delay
while True:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
if attempts >= max_retries:
raise
print(f"失败 ({attempts}/{max_retries}): {e}")
time.sleep(current_delay)
current_delay *= backoff # 指数退避
wrapper.retries = max_retries
wrapper.current_delay = current_delay
return wrapper
return decorator
class NetworkService:
@retry(max_retries=3, delay=0.5, backoff=2)
def fetch_data(self):
import random
if random.random() < 0.7: # 70% 概率失败
raise ConnectionError("网络超时")
return "成功获取数据"
service = NetworkService()
try:
print(service.fetch_data())
except ConnectionError as e:
print(f"最终失败:{e}")
缓存/记忆化装饰器
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
for i in range(30):
print(f"F({i}) = {fibonacci(i)}", end=" ")
print(f"\n缓存条目数:{fibonacci.cache_info()}") # 仅 30 个唯一输入
自定义记忆化装饰器:
def memoize(func):
"""通用记忆化装饰器"""
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
wrapper.cache_info = lambda: len(cache)
wrapper.clear_cache = lambda: cache.clear()
return wrapper
@memoize
def expensive_function(x):
total = sum(range(x * 1000000))
return total
print(expensive_function(5)) # 第一次:慢
print(expensive_function(5)) # 第二次:瞬间!(命中缓存)
expensive_function.clear_cache() # 清除缓存
权限验证装饰器
import functools
def require_role(*roles):
"""角色权限验证装饰器"""
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if user.get("role") not in roles:
raise PermissionError(
f"角色 '{user.get('role')}' 无权访问 {func.__name__}"
)
return func(user, *args, **kwargs)
return wrapper
return decorator
def check_permissions(required_level):
"""权限级别装饰器工厂"""
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
user_level = user.get("level", 0)
if user_level < required_level:
raise PermissionError(
f"需要 level≥{required_level},当前 {user_level}"
)
return func(user, *args, **kwargs)
return wrapper
return decorator
class AdminUser:
role = "admin"
level = 5
class RegularUser:
role = "user"
level = 1
@require_role("admin", "moderator")
@check_permissions(3)
def admin_panel(user, panel_name):
return f"管理员 {user['role']} 正在查看 {panel_name}"
try:
print(admin_panel(AdminUser(), "系统设置"))
# 管理员 admin 正在查看 系统设置
except PermissionError as e:
print(f"权限错误:{e}")
try:
print(admin_panel(RegularUser(), "系统设置"))
except PermissionError as e:
print(f"权限错误:{e}")
带参数的装饰器
import functools
def repeat(times=1):
"""重复执行指定次数"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
results = []
for i in range(times):
result = func(*args, **kwargs)
results.append(result)
print(f"执行 {i+1}/{times}")
return results if times > 1 else results[0]
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"你好,{name}!")
return name
greet("小明")
# 输出:
# 执行 1/3
# 你好,小明!
# 执行 2/3
# 你好,小明!
# 执行 3/3
# 你好,小明!
偏函数 partial
functools.partial 简介
偏函数 = 部分应用函数参数,返回一个新的函数对象。
from functools import partial
def power(base, exponent):
return base ** exponent
# 固定 exponent 参数,创建专用函数
square = partial(power, exponent=2) # x²
cube = partial(power, exponent=3) # x³
sqrt = partial(power, exponent=0.5) # √x
print(square(5)) # 25
print(cube(3)) # 27
print(sqrt(16)) # 4.0
# 底层等价于:lambda x: power(x, exponent=2)
偏函数实战场景
# 场景一:减少回调函数的参数数量
import time
def send_notification(to, message, method="email", timestamp=None):
if timestamp is None:
timestamp = time.time()
print(f"[{timestamp}] 发送 {method} 给 {to}: {message}")
# 为特定用户预配置通知方式
notify_alice_email = partial(send_notification, "alice@example.com", "密码重置链接", "email")
notify_bob_sms = partial(send_notification, "+86138xxxx", "验证码", "sms")
notify_alice_email()
notify_bob_sms()
# [1725684000.0] 发送 email 给 alice@example.com: 密码重置链接
# [1725684000.0] 发送 sms 给 +86138xxxx: 验证码
# 场景二:简化 map/filter/reduce 调用
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 传统方式
doubled = list(map(lambda x: x * 2, nums))
# 使用 partial(更简洁)
double = partial(lambda x, y: x * y, y=2)
doubled_v2 = list(map(double, nums))
print(doubled_v2) # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
装饰器组合
多个装饰器叠加
import functools
def logger(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[{func.__name__}] 开始")
result = func(*args, **kwargs)
print(f"[{func.__name__}] 结束")
return result
return wrapper
def counter(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
wrapper.call_count += 1
print(f"调用 #{wrapper.call_count}")
return func(*args, **kwargs)
wrapper.call_count = 0
return wrapper
@counter
@logger
def complex_operation():
print("正在进行计算...")
return sum(range(1_000_000))
complex_operation()
# 输出:
# 调用 #1
# [complex_operation] 开始
# 正在进行计算...
# [complex_operation] 结束
装饰器顺序的重要性
def uppercase(func):
def wrapper(s):
return func(s.upper())
return wrapper
def repeat_n(n):
def decorator(func):
def wrapper(s):
result = func(s)
return result * n
return wrapper
return decorator
# 顺序不同,行为完全不同
@repeat_n(2)
@uppercase
def shout(s):
return s + "!"
print(shout("hello")) # HELLO!HELLO!
# 如果反过来:
@uppercase
@repeat_n(2)
def whisper(s):
return s + "?"
print(whisper("hello")) # ???
常见问题与注意事项
陷阱一:装饰器改变函数签名
def trace(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper # ❌ 丢失了原始签名
@trace
def add(a, b):
pass
help(add) # wrapper 的签名而非 add 的
修复:使用 @functools.wraps(func)
陷阱二:异步函数不能用普通装饰器
import asyncio
async def slow_request(url):
await asyncio.sleep(1)
return f"Response from {url}"
# ❌ 普通装饰器无法正确包裹异步函数
# @logging_decorator
# async def fetch_page():
# pass
# ✅ 使用 aiocontextlib 或编写异步装饰器
陷阱三:闭包变量捕获延迟
# ❌ 所有装饰器共享同一个 loop_counter
loop_counters = []
for i in range(5):
def make_counter():
count = [0] # 被所有闭包共享
def increment():
count[0] += 1
return count[0]
return increment
loop_counters.append(make_counter())
# ✅ 每个装饰器应独立初始化
counters = [lambda x=i: x for i in range(5)]
总结
本文涵盖了装饰器与偏函数的核心知识:
- 装饰器基础 — 接收函数、返回新函数、
@语法糖 - wraps 装饰器 — 保留原始函数元数据
- 计时器 — 测量执行时间的通用装饰器
- 重试装饰器 — 指数退避的容错机制
- 缓存装饰器 — lru_cache/自定义记忆化
- 权限验证 — 角色/级别控制装饰器
- 偏函数 — partial 减少参数传递
- 装饰器组合 —
@decorator1then@decorator2 - 最佳实践 — wraps 不可少、异步需特殊处理
- 性能对比 — 装饰器有轻微 overhead,但提升开发效率显著
下一篇文章将深入讲解模块系统:导入、作用域与包结构。
本文是 Python 教程系列的第二十三篇。
