将Python函数哈希化以在修改函数时重新生成输出

23

我有一个Python函数,它能够产生确定性结果。该函数运行时间很长,并且生成大量的输出结果:

def time_consuming_function():
    # lots_of_computing_time to come up with the_result
    return the_result

我不时地修改time_consuming_function,但我希望在其未更改时避免再次运行它。[time_consuming_function仅依赖于在此考虑目的下是不可变的函数;即它可能具有来自Python库的函数,但不会来自我要更改的其他代码片段。] 我想到的解决方案是缓存输出并且缓存一些函数的“哈希”值。如果哈希值发生变化,则函数已被修改,我们必须重新生成输出。

这种做法可行还是荒谬?


更新:根据答案,看起来我想做的是“记忆化”time_consuming_function,除了(或者附加)传递给不变函数的参数之外,我还想考虑一个函数本身也会发生变化。


你如何修改这个方法?你想在程序运行期间保留哈希值,还是只在一个运行周期内但跨越一些模块重新加载时保留? - user319799
我会将该方法放在脚本文件中。我可能会不时手动修改它。应用是,该函数将生成可在某些仿真代码中运行的“问题数据”。我会不时更改这个问题。 - Seth Johnson
为什么不直接使用 doc 来检查函数的更改呢?每次修改函数时都进行更改。这既简单又鼓励(或者说强制)你跟踪更改。至于实际的记忆化,Mike Graham 为您提供了完美的解决方案。 - ktdrv
5个回答

6
如果我理解您的问题,我会像这样解决它。虽然有点狠,但我认为这比我在这里看到的其他解决方案更可靠和准确。
import inspect
import functools
import json

def memoize_zeroadic_function_to_disk(memo_filename):
    def decorator(f):
        try:
            with open(memo_filename, 'r') as fp:
                cache = json.load(fp)
        except IOError:
            # file doesn't exist yet
            cache = {}

        source = inspect.getsource(f)

        @functools.wraps(f)
        def wrapper():
            if source not in cache:
                cache[source] = f()
                with open(memo_filename, 'w') as fp:
                    json.dump(cache, fp)

            return cache[source]
        return wrapper
    return decorator

@memoize_zeroadic_function_to_disk(...SOME PATH HERE...)
def time_consuming_function():
    # lots_of_computing_time to come up with the_result
    return the_result

因此,唯一进行的哈希处理是Python内部字典键哈希处理,其中键是函数整个未编译代码的字符串值。是否有一种方法可以获取函数的编译代码,以便更改行间距或注释不会导致不同的值? - Seth Johnson
@Seth,在这里使用Python的内部哈希对我来说是有意义的,因为你真正想要的是比较值(以免出现哈希冲突而不知道)。虽然这很不可能,但也有可能。如果你只想缓存最近的函数,我根本不会使用字典或哈希,而是直接比较值。只有因为你(在外部)说你想存储多个版本的函数,所以你可以返回旧代码,我才使用了字典。 - Mike Graham
@Seth,存储比整个源代码少的信息应该是可能的——像空格和注释之类的保持不变,但需要小心确保您具有匹配所需的充分条件。f.func_code.co_code是函数实际存储的字节码,但我不确定它在编译之间是否相同。我也不能完全确定它不会给您带来错误的匹配结果。 - Mike Graham

1
与其将函数放在字符串中,我会将函数放在自己的文件中。例如,将其命名为time_consuming.py。它看起来会像这样:
def time_consuming_method():
   # your existing method here

# Is the cached data older than this file?
if (not os.path.exists(data_file_name) 
    or os.stat(data_file_name).st_mtime < os.stat(__file__).st_mtime):
    data = time_consuming_method()
    save_data(data_file_name, data)
else:
    data = load_data(data_file_name)

# redefine method
def time_consuming_method():
    return data

在测试基础设施以使其正常工作时,我会注释掉慢的部分。创建一个简单的函数,只返回0,确保所有保存/加载功能都能满足您的需求,然后再将慢的部分放回去。


0

所以,这里有一个使用装饰器的非常巧妙的技巧:

def memoize(f):
    cache={};
    def result(*args):
        if args not in cache:
           cache[args]=f(*args);
        return cache[args];
    return result;

有了上面的代码,你可以这样使用:

@memoize
def myfunc(x,y,z):
   # 一些非常耗时的计算

当你调用myfunc时,实际上是调用它的记忆化版本。相当不错,对吧?每当你想重新定义函数时,只需再次使用"@memoize",或者显式地编写:

myfunc = memoize(new_definition_for_myfunc);

编辑
我没有意识到你想在多次运行之间缓存。在这种情况下,你可以这样做:

import os;
import os.path;
import cPickle;
class MemoizedFunction(object): def __init__(self,f): self.function=f; self.filename=str(hash(f))+".cache"; self.cache={}; if os.path.exists(self.filename): with open(filename,'rb') as file: self.cache=cPickle.load(file);
def __call__(self,*args): if args not in self.cache: self.cache[args]=self.function(*args); return self.cache[args];
def __del__(self): with open(self.filename,'wb') as file: cPickle.dump(self.cache,file,cPickle.HIGHEST_PROTOCOL);
def memoize(f): return MemoizedFunction(f);

相当不错,但并没有回答问题。关键是方法中的代码发生了变化,而不是其输入参数。 - Lasse V. Karlsen
看起来 OP 想要在运行时之间记忆化函数的一个返回值。这会基于不同的参数缓存函数在一次运行中的各种返回值。 - Mike Graham
@Mike,好的。我没有意识到这是程序运行之间的事情。 - Michael Aaron Safyan
你当前的版本依赖于hash(f)在运行之间保持相同,但这既不是被保证的,也不是期望的。 - Mike Graham

0

第一部分是记忆化和序列化您的查找表。这应该基于一些Python序列化库足够简单。第二部分是,当源代码更改时,您希望删除序列化的查找表。也许这被过度思考成了一些花哨的解决方案。假设当您更改代码时,您会在某个地方检查它?为什么不将钩子添加到您的签入例程中,以删除序列化表格?或者如果这不是研究数据而是在生产中,则使其成为您的发布流程的一部分,如果文件的修订号(将此函数放在自己的文件中)已更改,则您的发布脚本将删除序列化的查找表。


-1

你所描述的实际上是记忆化。大多数常见函数可以通过定义装饰器来进行记忆化。

以下是一个(过于简化的)例子:

def memoized(f):
    cache={}
    def memo(*args):
        if args in cache:
            return cache[args]
        else:
            ret=f(*args)
            cache[args]=ret
            return ret
    return memo

@memoized
def time_consuming_method():
    # lots_of_computing_time to come up with the_result
    return the_result

编辑:

根据Mike Graham的评论和OP的更新,现在清楚了需要在程序的不同运行中缓存值。这可以通过使用一些持久性存储来实现缓存(例如使用Pickle或简单文本文件,或者可能使用完整的数据库,或介于两者之间的任何内容)。选择使用哪种方法取决于OP的需求。其他几个答案已经提供了一些解决方案,所以我不会在这里重复。


看起来 OP 想要在运行时之间记忆化函数的一个返回值。这会基于不同的参数缓存函数在一次运行中的各种返回值。 - Mike Graham

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接