如何实现临时功能" memoization"?

时间:2018-01-02 19:07:12

标签: python python-2.7 memoization

要记忆的功能不是"纯粹的" (它的返回值将来可能会改变)所以我不能使用iPhone X home indicator behavior装饰。 而且,我需要调用它的值列表。

我的工作是

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""[]{}和其他错误值不可能返回{{1}的值})。

这看起来更好吗?

2 个答案:

答案 0 :(得分:3)

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

x = cache[k] = expensive(k)

为:

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

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

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

如果你不喜欢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)) ,因此并不难。

def f(...):
    cacheorcompute = CacheDict(expensive)
    for ...:
        x = cacheorcompute[k]
        # use x here
    for x in cacheorcompute.itervalues():
        cleanup(x)

编写完该类后,您可以使用远远少于缓存的kruft编写函数:

corePoolSize

答案 1 :(得分:0)

ShadowRanger的答案可能就是您正在寻找的内容,但我也会考虑通过在一个地方进行设置和清理任务来进一步分离关注点,并利用x来解决问题。在其他地方使用contextlib.contextmanager

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)

所以这是一个更多的代码,但它为你提供了更多的可能性。