代码之家  ›  专栏  ›  技术社区  ›  sds Niraj Rajbhandari

如何实现临时功能“记忆化”?

  •  3
  • sds Niraj Rajbhandari  · 技术社区  · 7 年前

    要记忆的函数不是“纯”(其返回值将来可能会更改),因此我无法使用 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”方式。

    E、 例如,我可以通过书写保存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 ).

    这个看起来更好吗?

    2 回复  |  直到 7 年前
        1
  •  3
  •   ShadowRanger    7 年前

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

    x = cache[k] = expensive(k)
    

    收件人:

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

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

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

    如果您不喜欢函数本身中的所有kruft,那么一个很好的解决方法是使用您自己的 dict 遵循 collections.defaultdict (但使用键作为计算默认值的一部分)。这并不难,多亏了 __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)
    
        2
  •  1
  •   Rick SilentGhost    7 年前

    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)
    

    这是更多的代码,但它为您提供了更多的可能性。