要记忆的函数不是"纯"的(它的返回值将来可能会改变),所以我不能使用记忆装饰。 此外,我将需要调用它的值列表。
我所做的是
def f(...):
cache = {}
for ...:
try:
x = cache[k]
except KeyError:
x = cache[k] = expensive(k)
# use x here
for x in cache.itervalues():
cleanup(x)
我想知道这是否是表达范式的"pythonic"方式。
例如,我可以通过写作节省 3 行
def f(...):
cache = {}
for ...:
x = cache[k] = cache.get(k) or expensive(k)
# use x here
for x in cache.itervalues():
cleanup(x)
相反(假设None
、0
、""
、[]
、{}
和其他假值不可能返回expensive
的值)。
这样看起来更好吗?
我会坚持使用try
/except
版本,因为烘焙关于expensive
返回值为真实的假设对于泛化性来说是一个坏主意(在性能方面,作为实现细节,d[k]
比 CPython 上的d.get(k)
快,并且异常的成本通常与条件检查的成本相当, 更不用说所有这些可能是expensive
功能旁边的噪音)。不过,我会做一个调整,当两个线程竞争时,将结果统一化,并且最终都会计算昂贵的结果,以避免它们各自收到自己的(可能昂贵)结果副本。将except KeyError
处理程序中的行从:
x = cache[k] = expensive(k)
自:
x = cache.setdefault(k, expensive(k))
这样做,如果两个线程同时开始计算expensive
,第一个完成它将存储缓存的值,第二个将立即丢弃自己的结果,转而支持第一个存储的缓存值。如果结果只是计算成本高,内存或每个实例的其他资源成本不昂贵,这不会受到伤害,如果结果在其他方面很昂贵,这会快速消除重复的值。
除非k
是 C 级内置的,否则它实际上在 CPython 上并不是 100% 线程安全的(因为从理论上讲,在执行 Python 级别__eq__
函数以解决冲突时setdefault
可能会在真正的病理条件下触发一些竞争条件),但最坏的情况只是重复数据删除不起作用。
如果你不喜欢所有 kruft 都融入函数本身,一个很好的分解方法是滚动你自己的dict
子类,它遵循collections.defaultdict
的一般模式(但使用键作为计算默认值的一部分)。这并不难,这要归功于dict
提供的__missing__
钩子:
# Easiest to let defaultdict define the alternate constructor and attribute name
from collections import defaultdict
class CacheDict(defaultdict):
def __missing__(self, key):
# Roughly the same implementation as defaultdict's default
# __missing__, but passing the key as the argument to the factory function
return self.setdefault(key, self.default_factory(key))
编写该类后,您可以使用更少的缓存相关 kruft 编写函数:
def f(...):
cacheorcompute = CacheDict(expensive)
for ...:
x = cacheorcompute[k]
# use x here
for x in cacheorcompute.itervalues():
cleanup(x)
ShadowRanger 的答案可能是您正在寻找的,但我也会考虑通过在一个地方执行设置和清理任务来额外的关注点分离,并使用contextlib.contextmanager
在其他地方利用x
的工作:
from contextlib import contextmanager
@contextmanager
def xs_manager(...):
"""Manages setup/teardown of cache of x's"""
# setup
cache = {}
def gencache():
"""Inner generator for passing each x outside"""
for ...:
try:
x = cache[k]
except KeyError:
x = cache[k] = expensive(k)
yield x
yield gencache()
# external use of x's occurs here
# teardown
for x in cache.itervalues():
cleanup(x)
def f(...):
with xs_manager(...) as xvaluecache:
for x in xvaluecache:
# use x here
现在你当然可以这样做:
>>> f(...)
..但是,现在我们已经分离了设置/拆卸,如果我们想使用x
s(除了f
)执行我们以前可能没有考虑过的其他任务,包括g(x)
和h(x)
,我们可以稍后回到这段代码:
>>> with xs_manager(...) as xvaluecache:
... for x in xvaluecache:
... g(x)
... h(x)
所以这是更多的代码,但它为你设置了更多的可能性。