Python装饰器三层嵌套实战:手写一个带过期时间的缓存装饰器
【摘要】 在 Python 开发中,装饰器几乎无处不在:Web 框架的路由注册、测试框架的用例标记、functools 的缓存都离不开它。很多人能熟练使用 @lru_cache,却说不清"带参数的装饰器"为什么需要三层函数嵌套。今天我们亲手写一个带过期时间(TTL)的缓存装饰器,把这个知识点彻底讲透。 一、装饰器的本质:函数也是一种对象Python 里函数是一等公民:可以作为参数传递、作为返回值返回、...
在 Python 开发中,装饰器几乎无处不在:Web 框架的路由注册、测试框架的用例标记、functools 的缓存都离不开它。很多人能熟练使用 @lru_cache,却说不清"带参数的装饰器"为什么需要三层函数嵌套。今天我们亲手写一个带过期时间(TTL)的缓存装饰器,把这个知识点彻底讲透。
一、装饰器的本质:函数也是一种对象
Python 里函数是一等公民:可以作为参数传递、作为返回值返回、可以被重新赋值。装饰器的本质,就是"接收一个函数,返回一个增强后的新函数"的语法糖:
@timer
def slow():
...
# 完全等价于:slow = timer(slow)
无参装饰器只需要两层嵌套:外层接收被装饰的函数,内层 wrapper 负责增强逻辑。
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时 {time.time() - start:.3f}s")
return result
return wrapper
二、带参数时为什么需要三层嵌套?
当我们想写 @cache(ttl=60) 这种带参数的装饰器时,执行顺序变成了两步:先调用 cache(ttl=60) 得到真正的装饰器,再用它去装饰函数。于是必须再包一层:
def cache(ttl=60):
def decorator(func):
store = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(sorted(kwargs.items())))
now = time.time()
if key in store:
value, expire = store[key]
if now < expire:
return value # 缓存命中,直接返回
value = func(*args, **kwargs)
store[key] = (value, now + ttl)
return value
return wrapper
return decorator
三层各司其职:最外层接收装饰器参数,中间层接收被装饰的函数,最内层执行真正的增强逻辑。记住"想传几次参数就嵌套几层",就再也不会写错。
三、几个容易踩的坑
- 忘记 functools.wraps:不加它,被装饰函数的
__name__会变成wrapper,日志和文档工具就认不出原函数了。 - 可变参数别漏了:wrapper 一定要用
*args, **kwargs透传,否则装饰带参数的函数会直接报错。 - 缓存键要可哈希:上面用 args 加排序后的 kwargs 元组做键;如果参数里有字典、列表,需要先转成可哈希形式。
- 线程安全:多线程场景下读写 store 建议加锁;生产环境可以直接用
cachetools.TTLCache,它已经处理好了这些细节。
四、小结
装饰器写几层,取决于"你在调用装饰器时想传几次参数":无参两层、带参三层。理解了这个规律,权限校验、重试、限流这类装饰器都能举一反三。建议把上面的 TTL 缓存亲手敲一遍并跑通,装饰器这个知识点就真正属于你了。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)