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

时间:2018-06-19 20:15:33

标签: 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

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

是否可以检查Python函数中的更改?

1 个答案:

答案 0 :(得分:2)

假设您要问的是,是否有一种方法可以告诉您在上次退出程序与下次启动程序之间是否有人更改了函数的源代码……

无法直接执行此操作,但是如果您不介意受到一些黑客攻击,则手动进行操作并不难。

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

def source_match(source_path, object):
    try:
        with open(source_path) as f:
            source = f.read()
        if source == inspect.getsource(object):
            return True
    except Exception as e:
        # Maybe log e or something here, but any of the obvious problems,
        # like the file not existing or the function not being inspectable,
        # mean we have to re-generate the data
        pass
    return False

def load_data(serialize_pickle_path, original_filepath):
    invalid_hash = True
    if os.path.exists(serialize_pickle_path):
        if source_match(serialize_pickle_path + '.sourcepy', load_data_initial):
            content = mpu.io.read(serialize_pickle_path)
            data = content['data']
            invalid_hash = mpu.io.hash(original_filepath) != content['hash']
    # etc., but make sure to save the source when you save the pickle too

当然,即使函数的主体没有改变,其作用也可能由于例如某些模块常量的更改或它使用的某些其他函数的实现而改变。根据这有多重要,您可以提取定义在其中的整个模块,或者提取该模块以及它递归依赖的所有其他模块,等等。

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

此外,如果源不可用,因为它来自仅以.pyc格式分发的模块,则显然无法检查源。您可以选择该函数,或者仅访问其__code__属性。但是,如果该功能来自C扩展模块,那将是行不通的。那时,您最好的办法就是检查整个二进制文件的时间戳或哈希。

以及许多其他变体。但这足以让您入门。


另一种完全不同的选择是将检查作为开发工作流程的一部分,而不是作为程序的一部分。

假设您正在使用某种版本控制(如果没有,那么您应该这样做),其中大多数都带有某种提交挂钩系统。例如,a whole slew of options随附git。例如,如果您有一个名为.git/hooks/pre-commit的程序,则每次尝试git commit时该程序都会运行。

无论如何,最简单的预提交钩子就像(未经测试的):

#!/bin/sh
git diff --name-only | grep module_with_load_function.py && python /path/to/pickle/cleanup/script.py

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

如果您已经编辑了文件但是知道不需要清除泡菜,则可以git commit --no-verify。或者,您可以在脚本上扩展以具有一个环境变量,您可以使用该环境变量来跳过清理,或仅清理某些目录,或任何您想要的东西。 (最好默认不进行彻底清理,这是最坏的情况,当您每隔几周忘记一次时,您将浪费5分钟的等待时间,这并不比等待3小时让它对不正确的数据运行一堆处理要糟糕,对吧?

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

如果您对git一无所知(即使您不了解),则可能更愿意安装pre-commit之类的第三方库,从而更易于管理钩子,使用Python编写它们(无需处理复杂的git命令),等等。如果您对git感到满意,只需查看hooks--pre-commit.sample和一些templates目录中的其他示例应该足以为您提供想法。