Cython扩展不是通常的Python模块,因此底层操作系统的行为会透过。本答案涉及Linux,但其他操作系统也存在类似的行为/问题(好吧,Windows甚至不允许您重新构建扩展)。Cython扩展是共享对象。在导入时,CPython通过ldopen打开此共享对象,并调用init-function,即Python3中的PyInit_<module_name>,其中包括注册扩展提供的功能/函数等内容。如果加载了共享对象,则无法卸载它,因为可能存在一些Python对象仍然存活,这些对象将具有指向原始共享对象功能的悬空指针而不是函数指针。例如,请参见此
CPython-issue。另一个重要的事情是:当ldopen使用与已加载的共享对象相同的路径加载共享对象时,它将不会从磁盘上读取它,而只是重复使用已加载的版本,即使磁盘上有不同版本也是如此。
这就是我们方法的问题:只要生成的共享对象与旧对象具有相同的名称,您将无法在不重新启动解释器的情况下看到新功能。
你有哪些选择?
A: 使用
pyximport
和
reload_support=True
假设您的Cython (
foo.pyx
)模块如下所示:
def doit():
print(42)
doit()
现在使用pyximport导入它:
>>> import pyximport
>>> pyximport.install(reload_support=True)
>>> import foo
42
>>> foo.doit()
42
foo.pyx
已经被构建和加载(正如预期,我们可以看到它在加载时打印出42)。让我们来看一下foo
文件:
>>> foo.__file__
'/home/XXX/.pyxbld/lib.linux-x86_64-3.6/foo.cpython-36m-x86_64-linux-gnu.so.reload1'
您可以看到与使用
reload_support=False
构建的情况相比,增加了
reload1
后缀。通过查看文件名,我们还验证了路径中没有其他错误加载的
foo.so
文件。
现在,让我们将foo.pyx
中的42
更改为21
并重新加载文件:
>>> import importlib
>>> importlib.reload(foo)
21
>>> foo.doit()
42
>>> foo.__file__
'/home/XXX/.pyxbld/lib.linux-x86_64-3.6/foo.cpython-36m-x86_64-linux-gnu.so.reload2'
发生了什么?
pyximport
使用不同的前缀(
reload2
)构建了一个扩展并加载它。这是成功的,因为由于新前缀,新扩展的名称/路径不同,我们可以看到在加载时打印了
21
。
然而,
foo.doit()
仍然是旧版本!如果我们查阅
reload
-documentation,我们会看到:
当执行reload()时:
Python模块的代码被重新编译并且模块级别的代码被重新执行,
定义了一组新的对象,这些对象通过重用最初加载模块的加载器绑定到模块字典中的名称。
扩展模块的init函数
不会第二次调用。
init
(即PyInit_<module_name>
)不会为扩展程序执行(这也适用于Cython扩展),因此PyModuleDef_Init
与foo
-模块定义不会被调用,因此将卡在绑定到foo.doit
的旧定义上。这种行为是合理的,因为对于某些扩展,不应该调用init
函数两次。
要解决此问题,我们必须再次导入模块foo
:
>>> import foo
>>> foo.doit()
21
现在foo
已经重新加载得尽善尽美了-这意味着可能仍然有旧对象正在使用。但我相信您知道自己在做什么。
B:每个版本更改扩展名
另一种策略可以是将模块foo.pyx
构建为foo_prefix1.so
,然后是foo_prefix2.so
等等,并将其作为加载。
>>> import foo_perfixX as foo
这是IPython中%%cython魔法使用的策略,
它使用Cython代码的sha1哈希值作为前缀。您可以使用imp.load_dynamic(或其
实现与importlib的帮助)来模拟IPython的方法,因为imp已被弃用。
from importlib._bootstrap _load
def load_dynamic(name, path, file=None):
"""
Load an extension module.
"""
import importlib.machinery
loader = importlib.machinery.ExtensionFileLoader(name, path)
spec = importlib.machinery.ModuleSpec(
name=name, loader=loader, origin=path)
return _load(spec)
现在,我们可以将so文件放入不同的文件夹中(或添加一些后缀),这样dlopen
就会将它们视为与先前版本不同的文件,然后我们就可以使用它:
foo = load_dynamic("foo", "1/foo.cpython-37m-x86_64-linux-gnu.so")
foo = load_dynamic("foo", "2/foo.cpython-37m-x86_64-linux-gnu.so")
即使重新加载和特别是重新加载扩展有点hacky,出于原型设计的目的,我可能会选择pyximport
解决方案...或使用IPython和%%cython
魔法。
reload(x)
不能基于x
模块中的内容更新现有对象,并且不会替换任何对x
的其他引用(例如,如果模块y
也执行了import x
,那么它不会自动更新)。您需要非常彻底地更新所有引用x
的内容。 - DavidW