cython 嵌入后的 ImportError

Posted

技术标签:

【中文标题】cython 嵌入后的 ImportError【英文标题】:ImportError after cython embed 【发布时间】:2019-07-02 17:28:42 【问题描述】:

我无法通过已编译的 python 脚本看到其他可用的模块。我需要如何更改以下流程才能接受基于 venv 的模块或全局模块?

步骤:

$ python3 -m venv sometest
$ cd sometest
$ . bin/activate
(sometest) $ pip3 install PyCrypto Cython

基本脚本,使用非标准模块Crypto

# hello.py
from Crypto.Cipher import AES
import base64
obj = AES.new('This is a key123', AES.MODE_CBC, 'This is an IV456')
msg = "The answer is no"
ciphertext = obj.encrypt(msg)
print(msg)
print(base64.b64encode(ciphertext))
(sometest) $ python3 hello.py
The answer is no
b'1oONZCFWVJKqYEEF4JuL8Q=='

编译:

(sometest) $ cython -3 --embed hello.py
(sometest) $ gcc -Os -I /usr/include/python3.5m -o hello hello.c -lpython3.5m -lpthread -lm -lutil -ldl
(sometest) $ $ ./hello
Traceback (most recent call last):
  File "hello.py", line 1, in init hello
    from Crypto.Cipher import AES
ImportError: No module named 'Crypto'

我认为使用 cython-embedded-compiled 脚本中的 venv 没有问题:该脚本可以在没有 venv 的系统中的其他地方工作(也就是说,python3 -c 'from Crypto.Cipher import AES' 不会失败)。

否则该过程可以正常工作:

(sometest) $ echo 'print("hello world")' > hello2.py
(sometest) $ cython -3 --embed hello2.py
(sometest) $ gcc -Os -I /usr/include/python3.5m -o hello2 hello2.c -lpython3.5m -lpthread -lm -lutil -ldl
(sometest) $ ./hello2
hello world

系统:

(sometest) $ python3 --version
Python 3.5.2
(sometest) $ pip3 freeze
Cython==0.29.11
pkg-resources==0.0.0
pycrypto==2.6.1

(sometest) $ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.6 LTS"

【问题讨论】:

嵌入的python可能没有相同的pythonpath 就是这样,谢谢@ead。我曾假设它会使用相同的模块源,否则会起作用。现在,我看到PYTHONPATH=/usr/lib/python3/dist-packages/ ./hello 有效。虽然我觉得有点尴尬,就这么简单,但我觉得不删除它就足够了。如果你回答,我会接受。再次感谢您! 我希望您很清楚,在您的解决方案中,嵌入式 iterpreter 使用系统安装,而不是来自虚拟环境。 【参考方案1】:

通常,Python 解释器不是“独立的”,为了工作,它需要其标准库(例如 ctypes(已编译)或 site.py(已解释))以及其他站点包的路径(例如numpy)必须设置。

虽然可以通过冻结 py 模块并将所有 c 扩展(参见例如 this SO-post)合并到生成的可执行文件中来使 Python 解释器完全独立,但更容易为嵌入interpeter。可以从 python-homepage 下载“标准”安装所需的文件(至少对于windows),另请参见SO-question)。

有时查找标准模块/站点包不能开箱即用:必须通过设置 Python 路径来帮助解释器,即通过添加 <..>/sometest/lib/python3.5/site-packagessometest 是一个虚拟环境根文件夹)到sys.path 以编程方式在 pyx 文件中或通过在启动前设置 PYTHONPATH-环境变量。

继续阅读以了解更多详细信息和替代解决方案。


此答案适用于 Linux 和 Python3(Python 3.7),基本思想与 Windows/MacOS 相同,但某些细节可能有所不同。

因为使用了venv,所以我们有以下替代方案来解决问题:

<..>/sometest/lib/python3.5/site-packagessometest 是一个虚拟环境根文件夹)添加到sys.path,可以在 pyx 文件中以编程方式进行,也可以在开始之前设置PYTHONPATH-环境变量。 将嵌入了 python 的可执行文件放在sometest 的子目录中(例如bin 或创建自己的)。 使用virtualenv 而不是venv

注意:对于嵌入了python的可执行文件,无论是否激活了虚拟环境(或激活了哪个),它都不起任何作用。


为什么上述解决了您的场景中的问题?

问题是,(嵌入式)Python 解释器需要弄清楚以下内容的位置:

独立于平台的目录/文件,例如os.pyargparse.py(大部分都是 *.py/ *.pyc)。给定sys.prefix,解释器可以找出在哪里可以找到它们(即在prefix/lib/pythonX.Y)。 平台相关目录/文件,例如共享库。给定sys.exec_prefix,解释器可以找出在哪里可以找到它们(例如,共享库可以在exec_prefix/lib/pythonX.Y/lib-dynload 中找到)。

算法可以是found here,并在执行Py_Initialize时执行搜索。找到这些目录后,就可以构造sys.path了。

但是,当使用venv 时,有一个pyvenv.cfg-文件next to exe or in the parent directory,它可以确保找到正确的Python-Home - 一个很好的起点是此文件中的home-key。

如果Py_NoSiteFlag 没有设置,Py_Initialize 将使用site.py(它可以被解释器找到,因为sys.prefix 是已知的),或者更精确的site.main(),添加站点包虚拟环境到sys.path。这样做时,site.py 会查找 pyvenv.cfg 并对其进行解析。但是,只有在以下情况下才会将本地 site-packages 添加到 python-path:

如果上一个目录存在名为“pyvenv.cfg”的文件 sys.executable、sys.prefix 和 sys.exec_prefix 设置为 目录,并且还检查站点包(sys.base_prefix 并且 sys.base_exec_prefix 将始终是 Python 安装)。

在您的情况下,pyvenv.cfg 不在上面的目录中,但与 exe 相同 - 因此不包括通过 pip 安装库的本地站点包。不包括全局站点包,因为pyvenv.cfg 具有密钥include-system-site-packages = false。因此,不允许使用任何站点包,并且无法找到已安装的库。

但是,将 exe 下移一个目录,会导致将本地站点包包含到路径中。


还有其他可能的情况,重要的是可执行文件的位置,而不是激活的环境。

答:可执行文件在某处,但不在虚拟环境中

这种搜索启发式对已安装的 python 解释器或多或少可靠,但可能适用于嵌入式解释器或虚拟环境(有关更多信息,请参阅this issue)。

如果 python 是使用通常的apt install 或类似方法安装的,那么它将被找到(由于搜索算法中的4. step)并且嵌入式解释器将使用系统安装。

但是,如果文件被移动或 python 是从源代码构建但未安装,则嵌入的 interperter 无法启动:

Could not find platform independent libraries <prefix>
Could not find platform dependent libraries <exec_prefix>
Consider setting $PYTHONHOME to <prefix>[:<exec_prefix>]
Fatal Python error: initfsencoding: unable to load the file system codec
ModuleNotFoundError: No module named 'encodings'

在这种情况下,Py_SetPythonHome 或设置环境变量$PYTHONHOME 是可能的解决方案。

B:在虚拟环境中可执行,使用 virtualenv 创建

假设虚拟环境和嵌入式 python 的 Python 版本相同(否则我们有上述情况),嵌入式 exe 将使用本地侧包。由于this rule,家庭搜索算法总是会找到本地家庭:

步骤 3. 尝试找到相对于 argv0_path 的前缀和 exec_prefix,回溯路径直到用尽。这是成功的最常见步骤。请注意,如果 prefix 和 exec_prefix 是 不同的是,exec_prefix 更有可能被找到;但是,如果 exec_prefix 是prefix的子目录,都会找到。

在这种情况下,argv0_path 是 exe 的路径(没有 pyvenv.cfg 文件!),"landmarks"(lib/python$VERSION/os.py 和 lib/python$VERSION/lib- dynload) 将被找到,因为它们在 exe 上方的本地主页中显示为符号链接。

C:venv-environment 深处的两个可执行文件夹

venv-环境中将两个而不是一个文件夹(它工作的地方)向下移动会导致情况A:在搜索主页(太远)时未读取pyvenv.cfg文件,'venv`-environments缺少指向“地标”的符号链接(本地仅存在侧包),这样的第 3 步将失败,4. step 是唯一的希望。


推论:如果没有正确的 Python 安装,嵌入式 Python 将无法工作,除非有其他可能性:

所需的文件被打包到嵌入可执行文件旁边的lib\pythonX.Y\* 或上面的某个地方(并且没有pyvenv.cfg 周围会弄乱搜索)。

pyvenv.cfg 用于将解释器指向正确的位置。

【讨论】:

这需要考虑很多。我不知道venvvirtualenv 之间存在(显着)差异。当我尝试 outside 中的任何一个步骤时(假设模块全局可用,没有设置PYTHONPATH),它可以正常工作;这有助于我理解一些事情。您的回答我需要花一些时间才能接受,感谢您抽出宝贵的时间进行详尽的回答! 我知道嵌入式可执行文件仍然需要“完整”的 python 安装,但是当我运行 ./hello 时它失败了,尽管在可执行文件旁边有 lib/python3.5/site-packages/Crypto。假设可执行文件必须位于./lib/ 旁边的子目录中,是对“嵌入可执行文件旁边” 的更安全解释吗? (我没有意识到 venv 的细微差别,但现在我看到了行为差异。) @r2evans 如果周围没有 pyvenv.cfg 来搞乱搜索,那么“exe 旁边”就足够了(至少对于 Python3.7 - 不能用 Python3.5 测试它),但是您的建议更安全(将 exe 放在子文件夹中),因为它在 pyenv.cfg 附近时也有效。

以上是关于cython 嵌入后的 ImportError的主要内容,如果未能解决你的问题,请参考以下文章

利用Cython对python代码进行加密

如何在 cython 模块中使用外部包装类?

Cython 扩展模块类方法不可见/命名 提及时出错

在 C++ 程序中调用 Cython (Python3.6)

加载 instagram 嵌入后的 Javascript 回调?

嵌入层后的dropout层