如何重新加载 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 相关:
【讨论】:
以上是关于如何重新加载 Python3 C 扩展模块?的主要内容,如果未能解决你的问题,请参考以下文章