为何Python中用dict.setdefault实现的缓存装饰器重复调用函数?
为什么使用字典setdefault的装饰器无法正确缓存函数结果?
我尝试编写一个Python装饰器用于缓存函数执行结果。当cacheDecorator返回wrapper函数时,重复调用参数相同的f1(1,2,3)不会再次执行f1;但当返回使用字典setdefault方法的wrapper2时,即使传入相同参数,每次调用都会执行f1,不清楚原因。
复现代码
#!/bin/python import logging import sys logging.basicConfig( format="%(asctime)s [%(levelname)s] %(name)s - %(message)s", level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S", stream=sys.stdout, ) logger = logging.getLogger("mylogger") cache = {} # Relevant stuff begins here. def cacheDecorator(func): """ Goal is to decorate a function so that successive calls to it with the same arguments return the cached value, rather than invoking the function again unnecessarily. :param func: :return: cached value when possible """ def wrapper(*args, **kwargs): # This works global cache if args in cache: return cache.get(args) else: retVal = func(*args, **kwargs) cache[args] = retVal return retVal def wrapper2(*args, **kwargs): # This doesn't work - it always invokes func even on successive # calls with the same input arguments passed to f1. global cache return cache.setdefault(args,func(*args, **kwargs) ) #return wrapper # Causes f1 to be executed only once, as desired return wrapper2 # Causes f1 to be executed again even with same arguments. Why?? @cacheDecorator def f1 (a, b, c, /): logger.info('Inside f1') result = a * b * c return result logger.info (f'Result from executing f1(1,2,3) = {f1(1,2,3)}') logger.info (f'Result from executing f1(1,2,3) again = {f1(1,2,3)}') sys.exit(0)
问题原因
核心问题是Python的参数求值顺序:
调用cache.setdefault(args, func(*args, **kwargs))时,Python会先计算所有传入的参数值,再执行setdefault方法。也就是说,不管args是否已经在缓存里,func(*args, **kwargs)都会先被执行,把结果计算出来后才传给setdefault。这就导致每次调用wrapper2都会无条件执行原函数,完全起不到缓存的作用。
而wrapper的逻辑是先判断缓存是否存在:只有当args不在缓存中时,才会执行原函数并缓存结果,否则直接返回缓存值,这才符合缓存的预期行为。
修复方案
如果想用setdefault实现缓存,必须避免提前执行函数,但setdefault本身不支持懒加载参数,所以最直接的方式还是回到wrapper的判断逻辑:
def wrapper2(*args, **kwargs): global cache if args not in cache: cache[args] = func(*args, **kwargs) return cache[args]
或者直接使用Python标准库自带的functools.lru_cache装饰器,它已经实现了成熟的缓存逻辑,无需自己重复造轮子:
from functools import lru_cache @lru_cache(maxsize=None) def f1(a, b, c, /): logger.info('Inside f1') result = a * b * c return result
内容的提问来源于stack exchange,提问作者Lyle Z
相关产品推荐
相关产品推荐

