使用 cython 从多个 pyx 文件创建可执行文件
- 2025-03-04 08:24:00
- admin 原创
- 81
问题描述:
我正在尝试从我的 python 源文件创建一个 unix 可执行文件。
我有两个文件p1.py
,p2.py
p1.py:
from p2 import test_func
print (test_func())
p2.py:
def test_func():
return ('Test')
现在,我们可以看到p1.py
依赖于p2.py
。我想通过将两个文件合并在一起来制作一个可执行文件。我正在使用 cython。
我将文件名分别改为p1.pyx
和p2.pyx
。
现在,我可以使用 cython 使文件可执行,
cython p1.pyx --embed
它将生成一个名为的 C 源文件p1.c
。接下来我们可以使用 gcc 使其可执行,
gcc -Os -I /usr/include/python3.5m -o test p1.c -lpython3.5m -lpthread -lm -lutil -ldl
但是如何将两个文件合并为一个可执行文件?
解决方案 1:
您必须跳过一些循环才能使其工作。
首先,你必须意识到,生成的可执行文件是一个非常薄的层,它只是将整个工作委托给(即调用函数)pythonX.Ym.so
。你可以在调用时看到这种依赖关系
ldd test
...
libpythonX.Ym.so.1.0 => not found
...
因此,要运行该程序,您要么需要显示LD_LIBRARY_PATH
到的位置,libpythonX.Ym.so
要么使用选项构建 exe --rpath
,否则在动态加载程序启动时test
将引发类似的错误
/test:加载共享库时出错:libpythonX.Ym.so.1.0:无法打开共享对象文件:没有此文件或目录
通用构建命令如下所示:
gcc -fPIC <other flags> -o test p1.c -I<path_python_include> -L<path_python_lib> -Wl,-rpath=<path_python_lib> -lpython3.6m <other_needed_libs>
还可以针对 python 库的静态版本进行构建,从而消除对 libpythonX.Ym 的运行时依赖,例如参见此SO-post。
生成的可执行文件的test
行为与 Python 解释器完全相同。这意味着现在test
将失败,因为它找不到模块p2
。
一个简单的解决方案是就地对 p2 模块进行 cythonize(cythonize p2.pyx -i
):您将获得所需的行为 - 但是,您必须将生成的共享对象p2.so
与一起分发test
。
将两个扩展捆绑成一个可执行文件很容易 - 只需将两个 cythonized c 文件传递给 gcc:
# creates p1.c:
cython --empbed p1.pyx
# creates p2.c:
cython p2.pyx
gcc ... -o test p1.c p2.c ...
但是现在出现了一个新的(或旧的)问题:生成的test
可执行文件无法再次找到模块,因为在 python-path 上p2
没有p2.py
和 没有。p2.so
关于这个问题有两个类似的 SO 问题,这里和这里。 在你的情况下,提出的解决方案有点过头了,这里在将 p2 模块导入到p1.pyx
-file 之前初始化它就足以使其工作:
# making init-function from other modules accessible:
cdef extern object PyInit_p2();
#init/load p2-module manually
PyInit_p2() #Cython handles error, i.e. if NULL returned
# actually using already cached imported module
# no search in python path needed
from p2 import test_func
print(test_func())
如果模块之间存在循环依赖关系,在导入模块之前调用模块的 init 函数(实际上模块不会真正被第二次导入,只会在缓存中查找)也是可行的。例如,如果模块p2
导入了模块p3
,模块又会导入p2
。
警告:自 Cython 0.29 起,Cython 默认对 Python>=3.5 使用多阶段初始化,因此调用PyInit_p2
是不够的(例如,参见此 SO-post)。要关闭此多阶段初始化,-DCYTHON_PEP489_MULTI_PHASE_INIT=0
应传递给 gcc 或类似于其他编译器。
注意:但是,即使完成了上述所有操作,嵌入式解释器仍需要其标准库(例如,参见此SO-post)——要使其真正独立,还有很多工作要做!所以也许应该听取@DavidW 的建议:
“不要这样做”可能是绝大多数人最好的解决办法。
警告:如果我们声明PyInit_p2()
为
from cpython cimport PyObject
cdef extern PyObject *PyInit_p2();
PyInit_p2(); # TODO: error handling if NULL is returned
Cython 将不再处理错误,这是我们的责任。而不是
PyObject *__pyx_t_1 = NULL;
__pyx_t_1 = PyInit_p2(); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 4, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_1);
__Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
针对object
-version 生成,生成的代码变为:
(void)(PyInit_p2());
即没有错误检查!
另一方面使用
cdef extern from *:
"""
PyObject *PyInit_p2(void);
"""
object PyInit_p2()
无法与 g++ 一起使用 - 必须添加extern C
声明。
解决方案 2:
人们倾向于这样做,因为对于最简单的情况(一个模块,没有依赖关系)来说,这相当容易做到。@ead 的答案很好,但老实说相当繁琐,它正在处理下一个最简单的情况(两个你完全控制的模块,没有依赖关系)。
一般来说,Python 程序将依赖于一系列外部模块。Python 附带一个大型标准库,大多数程序都会在一定程度上使用它。有各种各样的第三方数学库、GUI 库和 Web 框架库。即使通过这些库跟踪这些依赖关系并找出需要构建的内容也很复杂,PyInstaller 等工具尝试了这一点,但并不是 100% 可靠。
在编译所有这些 Python 模块时,您可能会遇到一些 Cython 不兼容/错误。它通常相当不错,但在自省等功能方面存在困难,因此大型项目不太可能干净完整地编译。
最重要的是,许多模块都是用 C 语言或 SWIG、F2Py、Cython、boost-python 等工具编写的编译模块。这些编译模块可能有自己独特的特性,因此很难将它们链接在一起形成一个大块。
总而言之,这可能是可行的,但对于非平凡的程序来说,无论它看起来多么有吸引力,这都不是一个好主意。像 PyInstaller、Py2Exe 和 PyOxidizer 这样的工具使用更简单的方法(将所有内容捆绑到一个巨大的 zip 文件中),更适合这项任务(即使这样,它们也很难真正强大)。
请注意,发布此答案的目的是使该问题成为此问题的典型副本。虽然显示如何完成的答案很有用,但“不要这样做”可能是绝大多数人的最佳解决方案。
解决方案 3:
这不可能直接实现,但是您可以通过简单的破解来实现,只需根据需要创建多个文件,管理您的代码,然后在您的 setup.py 中将它们全部合并在一起并进行编译。
import os
# Check if file exists then remove it
if os.path.exists("merged.pyx"):
os.remove("merged.pyx")
else:
print("The file does not exist")
# merge.py
filenames = ['part1.pyx', 'part2.pyx', 'part3.pyx']
with open('merged.pyx', 'w') as outfile:
# This will clear the file before writing to it
outfile.truncate(0)
for fname in filenames:
with open(fname) as infile:
outfile.write(infile.read())
outfile.write('
') # Add a newline character at the end of each file
# Rest of your merge script...
非常简单,而且非常有效。
扫码咨询,免费领取项目管理大礼包!