【问题标题】:How to implement temporary function "memoization"?如何实现临时功能“memoization”?
【发布时间】:2018-06-12 11:34:17
【问题描述】:

要记忆的函数不是“纯”的(它的返回值将来可能会改变)所以我不能使用memoize 装饰。 此外,我需要调用它的值列表。

我做的是

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)

相反(假设None0""[]{} 和其他 false 值不可能是 expensive 的返回值)。

这样看起来更好吗?

【问题讨论】:

  • @timgeb:首先,我使用 python 2,而不是 python 3。其次,缓存必须是 本地 到我的函数 f
  • 所以要明确一点,每个cache 只能在其函数调用进行时存活?
  • @timgeb:是的,仅限本地
  • @glibdud:随便你怎么称呼它。问题是关于风格的。
  • 请注意,cache.get(k) 可能会返回除None 之外的其他虚假值,例如''0[]。经过一番考虑,我认为 try/except 方法很好。你也可以改用if k in cache

标签: python python-2.7 memoization


【解决方案1】:

我会坚持使用try/except 版本,因为假设expensive 的返回值是真实的,这对于泛化性来说是一个坏主意(在性能方面,作为一个实现细节,@ 987654324@ 在 CPython 上比 d.get(k) 快,并且异常的成本通常与条件检查的成本相当,更不用说所有这些都可能是 expensive 函数旁边的噪音)。不过,我会做一个调整,以在两个线程竞争时统一结果,并最终计算昂贵的结果,以避免它们各自收到自己的(可能是昂贵的)结果副本。将except KeyError 处理程序中的行从:

x = cache[k] = expensive(k)

到:

x = cache.setdefault(k, expensive(k))

这样做,如果两个线程同时开始计算expensive,第一个完成它的将存储缓存值,第二个将立即丢弃自己的结果以支持由存储的缓存值首先。如果结果只是计算成本高,而不是内存成本高或每个实例的其他资源成本高,这没有什么坏处,如果在其他方面成本高,这很快就会消除重复值。

除非k 是C 级别的内置函数,否则它在CPython 上实际上并不是100% 线程安全的(因为理论上,当执行Python 级别@ 时,setdefault 可能会在真正的病态条件下触发一些竞争条件987654333@ 解决冲突的函数),但最坏的情况是重复数据删除不起作用。

如果您不喜欢函数本身包含的所有 kruft,则将其分解的一个好方法是滚动您自己的 dict 子类,该子类遵循 collections.defaultdict 的一般模式(但使用密钥作为一部分计算默认值)。这并不难,感谢__missing__ 钩子dict 提供:

# 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)

【讨论】:

  • 我喜欢你的 CacheDict 建议!
【解决方案2】:

ShadowRanger 的答案可能是您正在寻找的,但我也会考虑通过在一个地方执行设置和清理任务,并使用@987654321 将使用xs 的工作放在其他地方,从而进一步分离关注点@:

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(...)

..但是,既然我们已经分离了设置/拆卸,如果我们想使用 xs(f 以外)执行我们可能没有的其他任务,我们可以稍后再回到这段代码之前考虑过,包括g(x)h(x)

>>> with xs_manager(...) as xvaluecache:
...    for x in xvaluecache:
...        g(x)
...        h(x)

所以这是一个多一点的代码,但它让你有更多的可能性。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-04
    • 1970-01-01
    • 2011-12-14
    • 2019-04-16
    • 2021-01-08
    相关资源
    最近更新 更多