如何重新加载 Python3 C 扩展模块?

Posted

技术标签:

【中文标题】如何重新加载 Python3 C 扩展模块?【英文标题】:How to Reload a Python3 C extension module? 【发布时间】:2012-01-07 21:28:58 【问题描述】:

我为 Python 3.2 编写了一个 C 扩展 (mycext.c)。该扩展依赖于存储在 C 头文件 (myconst.h) 中的常量数据。头文件由 Python 脚本生成。在同一个脚本中,我使用了最近编译的模块。 Python3 myscript中的工作流程(未完整展示)如下:

configure_C_header_constants() 
write_constants_to_C_header() # write myconst.h
os.system('python3 setup.py install --user') # compile mycext
import mycext
mycext.do_stuff()

这是第一次在 Python 会话中完美运行。如果我在同一会话中重复该过程(例如,在单元测试的两个不同测试用例中),则始终(重新)加载 mycext 的第一个编译版本。

如何有效地重新加载具有最新编译版本的扩展模块?

【问题讨论】:

如果您需要一直更改它,它并不完全是常量...将常量放在配置文件中。 它们在实际应用程序中将保持不变(它不会使用 Python)。我使用 Python 生成常量并对 C 代码进行单元测试。 制作一个配置文件,直到你弄清楚常量应该是什么。 感谢您的建议。我正在测试一种算法,常量是特定于应用程序的(我事先无法知道它们)。从我不完整的问题描述中,不清楚为什么我不能按照您的建议进行操作。不过,Sven 提供的答案正是我想要的。 确实不清楚,因为没有原因。你可以那样做,我保证。 :-) 【参考方案1】:

您可以使用imp.reload() 函数在 Python 3.x 中重新加载模块。 (这个函数曾经是 Python 2.x 的内置函数。请务必阅读文档——有一些注意事项!)

Python 的导入机制永远不会dlclose() 共享库。加载后,库将一直保留到进程终止。

您的选择(按有用性递减排序):

    将模块导入移动到子进程,并在重新编译后再次调用子进程,即您有一个 Python 脚本 do_stuff.py 就可以了

    import mycext
    mycext.do_stuff()
    

    你调用这个脚本使用

    subprocess.call([sys.executable, "do_stuff.py"])
    

    将标头中的编译时常量转换为可从 Python 更改的变量,无需重新加载模块。

    在删除对模块的所有引用后手动 dlclose() 库(有点脆弱,因为您自己没有保存所有引用)。

    滚动您自己的导入机制。

    这是一个如何做到这一点的示例。我写了一个最小的 Python C 扩展 mini.so,只导出一个名为 version 的整数。

    >>> import ctypes
    >>> libdl = ctypes.CDLL("libdl.so")
    >>> libdl.dlclose.argtypes = [ctypes.c_void_p]
    >>> so = ctypes.PyDLL("./mini.so")
    >>> so.PyInit_mini.argtypes = []
    >>> so.PyInit_mini.restype = ctypes.py_object 
    >>> mini = so.PyInit_mini()
    >>> mini.version
    1
    >>> del mini
    >>> libdl.dlclose(so._handle)
    0
    >>> del so
    

    此时,我将mini.c中的版本号递增并重新编译。

    >>> so = ctypes.PyDLL("./mini.so")
    >>> so.PyInit_mini.argtypes = []
    >>> so.PyInit_mini.restype = ctypes.py_object 
    >>> mini = so.PyInit_mini()
    >>> mini.version
    2
    

    可以看到使用的是新版本的模块。

    供参考和实验,这里是mini.c

    #include <Python.h>
    
    static struct PyModuleDef minimodule = 
       PyModuleDef_HEAD_INIT, "mini", NULL, -1, NULL
    ;
    
    PyMODINIT_FUNC
    PyInit_mini()
    
        PyObject *m = PyModule_Create(&minimodule);
        PyModule_AddObject(m, "version", PyLong_FromLong(1));
        return m;
    
    

【讨论】:

谢谢,imp.reload(mypythonmod) 适用于 Python 模块,但我正在处理 C 扩展模块。 imp.reload(mycext) 仍然会重新加载最初导入的扩展模块版本。 您能否详细说明选项 1。我对子流程没有任何经验。我试过subprocess.call(['import', 'mycext']) 并且解释器保持空闲状态。试过subprocess.Popen(['import', 'mycext'],然后我怎么打电话给mycext.do_stuff() @user1069152:编辑了我的答案。 选项 4 完美运行。当 do_stuff 需要输入和输出参数时,选项 1 变得复杂。 @user1069152:嗯,选项 4 是一个非常简单的方法,而且不是很便携。为了使更健壮的选项 1 起作用,您可以使用 pickle 模块将参数传递给子进程并取回返回值。【参考方案2】:

还有另一种方法,设置一个新的模块名称,导入它,然后更改对它的引用。

【讨论】:

请您澄清一下或者举个例子好吗?【参考方案3】:

更新:我现在围绕这种方法创建了一个 Python 库:

https://github.com/bergkvist/creload https://pypi.org/project/creload/

您可以使用multiprocessing,而不是在 Python 中使用 subprocess 模块。这允许子进程继承父进程的所有内存(在 UNIX 系统上)。

因此,您还需要注意不要将 C 扩展模块导入父级。

如果您返回一个依赖于 C 扩展的值,它还可能会强制将 C 扩展导入到父级中,因为它接收到函数的返回值。

import multiprocessing as mp
import sys


def subprocess_call(fn, *args, **kwargs):
    """Executes a function in a forked subprocess"""
    
    ctx = mp.get_context('fork')
    q = ctx.Queue(1)
    is_error = ctx.Value('b', False)
    
    def target():
        try:
            q.put(fn(*args, **kwargs))
        except BaseException as e:
            is_error.value = True
            q.put(e)
    
    ctx.Process(target=target).start()
    result = q.get()    
    if is_error.value:
        raise result
    
    return result


def my_c_extension_add(x, y):
    assert 'my_c_extension' not in sys.modules.keys()
    # ^ Sanity check, to make sure you didn't import it in the parent process

    import my_c_extension
    return my_c_extension.add(x, y)


print(subprocess_call(my_c_extension_add, 3, 4))

如果您想将其提取到装饰器中 - 为了获得更自然的感觉,您可以这样做:

class subprocess:
    """Decorate a function to hint that it should be run in a forked subprocess"""
    def __init__(self, fn):
        self.fn = fn
    def __call__(self, *args, **kwargs):
        return subprocess_call(self.fn, *args, **kwargs)


@subprocess
def my_c_extension_add(x, y):
    assert 'my_c_extension' not in sys.modules.keys()
    # ^ Sanity check, to make sure you didn't import it in the parent process

    import my_c_extension
    return my_c_extension.add(x, y)


print(my_c_extension_add(3, 4))

如果您在 Jupyter 笔记本中工作,并且想要重新运行某些功能而不重新运行所有现有单元格,这可能会很有用。

注意事项

此答案可能仅与您有 fork() 系统调用的 Linux/macOS 相关:

Python multiprocessing linux windows difference https://rhodesmill.org/brandon/2010/python-multiprocessing-linux-windows/

【讨论】:

以上是关于如何重新加载 Python3 C 扩展模块?的主要内容,如果未能解决你的问题,请参考以下文章

python 3.x C 扩展模块和子模块

如何扩展 Python 并制作 C 包?

nginx已安装完毕,如何再添加第三方模块?

在 python 2 上释放 C 扩展模块时运行函数

如何自动重新加载我正在开发的 Chrome 扩展程序?

PCB硬件开源天问ASRPRO语音模块扩展板