我有一个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。
答案 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.bar
与pyximporter
模块一起发挥作用,然后为您重新编译。我还向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:将pyximport
与reload_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。