用pyximport重新加载模块吗?

时间:2019-03-08 04:05:26

标签: python cython

我有一个Python程序,该程序在运行之前会加载大量数据。因此,我希望能够在不重新加载数据的情况下重新加载代码。使用常规python,importlib.reload可以正常工作。这是一个示例:

setup.py:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

extensions = [
    Extension("foo.bar", ["foo/bar.pyx"],
              language="c++",
              extra_compile_args=["-std=c++11"],
              extra_link_args=["-std=c++11"])
]
setup(
    name="system2",
    ext_modules=cythonize(extensions, compiler_directives={'language_level' : "3"}),
)

foo / bar.py

cpdef say_hello():
    print('Hello!')

runner.py:

import pyximport
pyximport.install(reload_support=True)

import foo.bar
import subprocess
from importlib import reload

if __name__ == '__main__':

    def reload_bar():
        p = subprocess.Popen('python setup.py build_ext --inplace',
                             shell=True,
                             cwd='<your directory>')
        p.wait()

        reload(foo.bar)
        foo.bar.say_hello()

但这似乎不起作用。如果我编辑bar.pyx并运行reload_bar,则看不到我的更改。我也尝试过pyximport.build_module(),但运气不佳-重建了模块但没有重新加载。我在“普通” python外壳中运行,如果有区别,则不是IPython。

2 个答案:

答案 0 :(得分:2)

与Python 3.x相比,我能够轻松地为Python 2.x工作。无论出于何种原因,Cython似乎都在缓存它从中导入模块的可共享对象(.so)文件,即使在运行时重建并删除旧文件后,它仍会从旧的可共享对象文件中导入。但是,无论如何这都是没有必要的(当您import foo.bar时,它不会创建一个),因此我们还是可以跳过这一步。

最大的问题是,即使在reload之后,python仍然保留了对旧模块的引用。普通的python模块似乎可以找到,但是与cython无关。为了解决这个问题,我运行了两个语句来代替reload(foo.bar)

del sys.modules['foo.bar']
import foo.bar

此操作成功(尽管效率可能较低)重新加载了cython模块。运行该子进程的Python 3.x中唯一存在的问题是创建有问题的可共享对象。取而代之的是,全部跳过,让import foo.barpyximporter模块一起发挥作用,然后为您重新编译。我还向pyxinstall命令添加了一个选项,以指定与您在setup.py中指定的语言级别相匹配的语言级别

pyximport.install(reload_support=True, language_level=3)

所以在一起:

runner.py

import sys
import pyximport
pyximport.install(reload_support=True, language_level=3)

import foo.bar

if __name__ == '__main__':
    def reload_bar():
        del sys.modules['foo.bar']
        import foo.bar

    foo.bar.say_hello()
    input("  press enter to proceed  ")
    reload_bar()
    foo.bar.say_hello()

其他两个文件保持不变

运行:

Hello!
  press enter to proceed

"Hello!"替换 foo / bar.pyx 中的"Hello world!",然后按 Enter

Hello world!

答案 1 :(得分:2)

Cython扩展不是通常的python模块,因此底层OS的行为微不足道。这个答案是关于Linux的,但其他操作系统也有类似的行为/问题(好吧,Windows甚至不允许您重建扩展名)。

cython扩展名是共享对象。 CPython通过ldopen打开此共享库,并调用init函数,即Python3中的PyInit_<module_name>,该函数注册了扩展提供的功能。

最重要的是:当ldopen加载与一个已加载的共享对象具有相同路径的共享对象时,它将不会从光盘中读取它,而只是重用已经加载的版本-即使它是光盘上的其他版本。

这是我们方法的问题:只要生成的共享库名称与旧的共享库名称相同,您就不会在不重新启动解释器的情况下看到新功能。

您有什么选择?

A:将pyximportreload_support=True一起使用

假设您的Cython(foo.pyx)模块如下所示:

def doit(): 
    print(42)
# called when loaded:
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'

与使用reload1构建的情况相比,您可以看到其他reload_support=False前缀。看到文件名,我们还验证了路径中某处没有其他foo.so并被错误加载。

现在让我们将42中的21更改为foo.pyx并重新加载文件:

>>> 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扩展),因此具有foo-module-definition的PyModuleDef_Init是' t被调用,并且被绑定到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-magic使用的策略,以Cython代码的which uses sha1-hash作为前缀。


即使特别是重新加载和重新加载扩展名有点hacky,出于原型设计的目的,我也可能会选择pyximport-solution ...或使用IPython和%%cython-magic。