如何实现Python函数缓存包装器?基于参数哈希复用ML确定性函数结果
嘿,这个需求在机器学习场景里太实用了——毕竟谁也不想反复跑那些耗时又固定的计算对吧!直接用装饰器就完美解决,比包装类简洁灵活得多,完全贴合Python的惯用写法。
核心思路
我们要实现一个带参数的装饰器,核心逻辑是:
- 每次调用被装饰的函数时,先把传入的参数(包括位置参数、关键字参数,甚至默认参数)转换成唯一的哈希值
- 用「函数名+哈希值」作为pickle缓存文件的名字
- 如果缓存文件存在,直接加载返回结果;不存在就执行原函数,再把结果存入缓存
完整实现代码
import pickle import hashlib import os import inspect def cached_with_pickle(cache_dir="./function_cache/"): """ 给确定性函数添加pickle缓存的装饰器 :param cache_dir: 缓存文件存储的目录,默认是当前目录下的function_cache文件夹 """ def decorator(func): # 确保缓存目录存在,不存在则创建 os.makedirs(cache_dir, exist_ok=True) def wrapper(*args, **kwargs): # 绑定函数参数,确保默认参数也被纳入哈希计算 sig = inspect.signature(func) bound_args = sig.bind(*args, **kwargs) bound_args.apply_defaults() # 将绑定后的参数序列化为字节,用于生成唯一哈希 args_bytes = pickle.dumps((bound_args.args, bound_args.kwargs)) # 用MD5生成短哈希值(也可以用SHA256,更安全但文件名更长) hash_obj = hashlib.md5(args_bytes) hash_str = hash_obj.hexdigest() # 生成唯一的缓存文件名(加入函数名避免不同函数参数冲突) cache_file = os.path.join(cache_dir, f"{func.__name__}_{hash_str}.pkl") # 检查缓存是否存在 if os.path.exists(cache_file): print(f"✅ 加载缓存结果: {cache_file}") with open(cache_file, "rb") as f: return pickle.load(f) # 缓存不存在,执行原函数 print(f"🔄 执行计算(无缓存)...") result = func(*args, **kwargs) # 保存结果到缓存 print(f"💾 保存结果到缓存: {cache_file}") with open(cache_file, "wb") as f: pickle.dump(result, f) return result return wrapper return decorator
使用示例
比如你有一个训练模型的确定性函数,直接加装饰器就行:
@cached_with_pickle(cache_dir="./ml_model_cache/") def train_ml_model(learning_rate, batch_size, epochs): # 模拟耗时的模型训练过程 import time time.sleep(3) return {"accuracy": 0.9 + learning_rate * 0.1, "total_epochs": epochs} # 第一次调用:会执行计算并保存缓存 print(train_ml_model(0.01, 32, 10)) # 第二次调用相同参数:直接加载缓存,瞬间返回结果 print(train_ml_model(0.01, 32, 10))
关键注意事项
- 确定性函数前提:必须保证函数是完全确定性的——相同输入一定输出完全一致的结果,否则缓存会返回错误结果
- 参数可序列化:如果你的参数里有自定义类实例,要确保这些实例能被pickle序列化(或者自己实现
__getstate__/__setstate__方法) - 缓存安全:如果缓存目录是公共环境,要注意恶意pickle文件的风险,生产环境可以考虑加入哈希校验,或者用更安全的序列化方式(比如
msgpack) - 缓存清理:可以定期清理旧的缓存文件,避免磁盘占用过大——比如给装饰器加一个
expire_hours参数,自动跳过过期的缓存
为什么选装饰器而不是包装类?
装饰器是这个场景的最优选择:
- 语法简洁:只需要在函数上方加一行
@装饰器,几乎零侵入性,代码可读性拉满 - 灵活参数化:通过嵌套装饰器可以轻松实现自定义缓存目录、过期时间等配置
- 符合Python风格:函数式编程的写法更贴合Python社区的惯用方式,团队协作时更容易被理解
如果用包装类实现,虽然功能也能完成,但需要把函数实例化成类对象,调用方式会变得繁琐(比如cached_func = CacheWrapper(train_model)再调用),远不如装饰器直观。
内容的提问来源于stack exchange,提问作者arm
相关产品推荐
相关产品推荐

