如何检查加载的 Python 函数是否发生变化?

Mar*_*oma 2 python

作为一名数据科学家/机器学习开发人员,我大部分时间(总是?)都有一个load_data函数。执行该函数通常需要超过 5 分钟,因为执行的操作成本很高。当我将最终结果存储load_data在 pickle 文件中并再次读取该文件时,时间通常会缩短到几秒钟。

所以我经常使用的一个解决方案是:

def load_data(serialize_pickle_path, original_filepath):
    invalid_hash = True

    if os.path.exists(serialize_pickle_path):
        content = mpu.io.read(serialize_pickle_path)
        data = content['data']
        invalid_hash = mpu.io.hash(original_filepath) != content['hash']

    if invalid_hash:
        data = load_data_initial()
        filehash = mpu.io.hash(original_filepath)
        mpu.io.write(serialize_pickle_path, {'data': data, 'hash': filehash})

    return data
Run Code Online (Sandbox Code Playgroud)

该解决方案有一个主要缺点:如果发生load_data_initial更改,文件将不会重新加载。

有没有办法检查Python函数的变化?

aba*_*ert 6

假设您询问是否有办法判断在您上次退出程序和下次启动程序之间是否有人更改了该函数的源代码\xe2\x80\xa6

\n\n

没有办法直接执行此操作,但如果您不介意稍微搞一点技巧,那么手动执行此操作并不难。

\n\n

由于您已经import编辑了该模块并有权访问该函数,因此您可以使用该getsource函数来获取其源代码。因此,您所需要做的就是保存该源。例如:

\n\n
def source_match(source_path, object):\n    try:\n        with open(source_path) as f:\n            source = f.read()\n        if source == inspect.getsource(object):\n            return True\n    except Exception as e:\n        # Maybe log e or something here, but any of the obvious problems,\n        # like the file not existing or the function not being inspectable,\n        # mean we have to re-generate the data\n        pass\n    return False\n\ndef load_data(serialize_pickle_path, original_filepath):\n    invalid_hash = True\n    if os.path.exists(serialize_pickle_path):\n        if source_match(serialize_pickle_path + \'.sourcepy\', load_data_initial):\n            content = mpu.io.read(serialize_pickle_path)\n            data = content[\'data\']\n            invalid_hash = mpu.io.hash(original_filepath) != content[\'hash\']\n    # etc., but make sure to save the source when you save the pickle too\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

当然,即使函数体没有改变,它的效果也可能会因为某些模块常量的变化或它使用的其他函数的实现而改变。根据这有多重要,您可以引入它定义的整个模块,或者该模块加上它递归依赖的每个其他模块,等等。

\n\n

当然,您也可以保存文本的哈希值而不是全文,以使内容变得更小。或者将它们嵌入到 pickle 文件中,而不是将它们保存在一起。

\n\n

另外,如果源不可用,因为它来自您仅以.pyc格式分发的模块,那么您显然无法检查源。您可以腌制该函数,或者仅访问其__code__属性。但如果该函数来自 C 扩展模块,则即使这样也不起作用。此时,您能做的最好的事情就是检查整个二进制文件的时间戳或哈希值。

\n\n

还有很多其他变化。但这应该足以让您开始。

\n\n
\n\n

一种完全不同的替代方案是将检查作为开发工作流程的一部分,而不是作为程序的一部分。

\n\n

假设您正在使用某种版本控制(如果没有,您确实应该使用),其中大多数都带有某种提交挂钩系统。例如,git带有大量选项。例如,如果您有一个名为 的程序.git/hooks/pre-commit,则每次您尝试运行该程序时,它都会运行git commit

\n\n

无论如何,最简单的预提交挂钩类似于(未经测试):

\n\n
#!/bin/sh\ngit diff --name-only | grep module_with_load_function.py && python /path/to/pickle/cleanup/script.py\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在,每次执行 a 时git commit,如果差异包含对名为的文件的任何更改module_with_load_function.py(显然使用其中的文件名load_data_initial),它将首先运行脚本/path/to/pickle/cleanup/script.py(这是您编写的脚本,仅删除所有缓存的泡菜文件)。

\n\n

如果您已编辑该文件,但知道不需要清除泡菜,则可以直接git commit --no-verify. 或者,您可以扩展脚本以拥有一个环境变量,您可以使用该环境变量来跳过清理,或者仅清理某些目录,或者您想要的任何内容。(最好默认过度热心地清理\xe2\x80\x94最坏的情况,当你每隔几周忘记一次时,你会浪费 5 分钟的等待时间,这并不比等待 3 个小时让它运行那么糟糕对不正确数据的一堆处理,对吧?)

\n\n

您可以对此进行扩展,例如检查完整的差异并查看它们是否包含该功能,而不仅仅是检查文件名。钩子只是任何可执行文件,因此如果它们太复杂,您可以用 Python 而不是 bash 编写它们。

\n\n

如果您不太了解git(或者即使您了解),您可能会更高兴安装这样的第三方库,这样pre-commit可以更轻松地管理挂钩,用 Python 编写它们(无需处理git如果您熟悉git只需查看目录hooks--pre-commit.sample中的一些其他示例templates就足以给您带来想法。

\n