使用Cython嵌入后出现ImportError

7

我无法让编译的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编译嵌入式脚本的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"

1
嵌入式Python可能没有相同的Python路径。 - ead
就是这样,谢谢 @ead。我假设它会使用同样的模块源,否则无法工作。现在,我发现 PYTHONPATH=/usr/lib/python3/dist-packages/ ./hello 可以工作。虽然我感到有点尴尬,因为它是如此简单,但我认为它足够有价值,不应该删除。如果您回答,我会接受。再次感谢! - r2evans
我希望你明白,使用你的解决方案时,嵌入式解释器使用的是系统安装而不是虚拟环境。 - ead
1个回答

5
通常情况下,Python解释器并不是“独立的”,为了正常工作,它需要其标准库(例如编译库ctypes或解释库site.py)以及其他站点包的路径(例如numpy)必须设置好。
虽然可以通过冻结py模块并合并所有c扩展(例如this SO-post)到生成的可执行文件中,使Python解释器完全独立,但更容易提供所需的安装程序给嵌入式解释器。可以从Python主页下载所需的“标准”安装文件(至少对于windows),也可以参考这个SO-question
有时找到标准模块/站点包并不能立即使用:必须通过设置Python-path来帮助解释器,例如向sys.path中添加<..>/sometest/lib/python3.5/site-packages(其中sometest是虚拟环境根文件夹),要么在pyx文件中以编程方式添加,要么在启动之前通过设置PYTHONPATH环境变量来添加。更多详细信息和替代方案请接着阅读。

本答案适用于Linux和Python3(Python 3.7),基本思路对于Windows/MacOS也是相同的,但某些细节可能会有所不同。

因为使用了venv,我们有以下解决方法:

  • 在pyx文件中编程或在启动前设置PYTHONPATH环境变量,将<..>/sometest/lib/python3.5/site-packages(其中sometest是虚拟环境根目录)添加到sys.path中。
  • 将带有嵌入式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中找到)。

算法可以在这里找到,当执行Py_Initialize时进行搜索。一旦找到这些目录,就可以构建sys.path

然而,使用venv时,有一个pyvenv.cfg文件在exe旁边或父目录中, 确保找到正确的Python-Home - 这个文件中的home键是一个很好的起点。

如果未设置Py_NoSiteFlagPy_Initialize 将利用 site.py(它可以通过解释器找到,因为已知sys.prefix),或更精确地说,利用 site.main(),将虚拟环境的 site-packages 添加到 sys.path。在此过程中,site.py 查找 pyvenv.cfg 并解析它。但是,仅当以下情况时,本地的 site-packages 才会添加到 python-path 中:
如果存在一个名为“pyvenv.cfg”的文件在sys.executable的上一级目录中,那么sys.prefix和sys.exec_prefix将被设置为该目录,并且还会检查其中是否包含site-packages(sys.base_prefix和sys.base_exec_prefix始终是Python安装的“真实”前缀)。
在您的情况下,pyvenv.cfg不在上面的目录中,而是与exe文件在同一目录中 - 因此通过pip安装的库所在的本地site-packages未包括在内。全局site-packages未包括在内,因为pyvenv.cfg具有键include-system-site-packages = false。因此,不允许使用site-packages,因此找不到安装的库。
但是,将exe文件移动到下一个目录中,将导致包含本地site-packages到路径中。

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

A: 可执行文件在某个地方,但不在虚拟环境中

这种搜索启发式算法对于已安装的python解释器来说比较可靠,但对于嵌入式解释器或虚拟环境可能会出现问题(请参见this issue获取更多信息)。

如果使用常规的apt install或类似方法安装了Python,则会被找到(由于搜索算法中的第4步),并且嵌入式解释器将使用系统安装。

但是,如果文件被移动或Python是从源代码构建但未安装,则无法启动嵌入式解释器:

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 版本相同(否则我们就有了上面的情况),嵌入式可执行文件将使用本地侧包。由于 此规则,主目录搜索算法将始终找到本地主目录:

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

在这种情况下,argv0_path是exe文件的路径(没有pyvenv.cfg文件!),并且"landmarks"(lib/python$VERSION/os.py和lib/python$VERSION/lib-dynload)将被找到,因为它们作为符号链接出现在本地主目录上方的exe文件中。 C:venv环境中深入两个文件夹的可执行文件 进入venv环境中深入两个文件夹(而不是一个文件夹,在那里它可以正常工作)会导致情况A:在搜索主目录时不会读取pyvenv.cfg文件(太远了),'venv`-environments缺乏到“landmarkers”的符号链接(仅在本地存在边缘包),因此步骤3将失败,第4步是唯一的希望。

推论:除非满足以下情况之一,否则嵌入式Python将无法正常工作:

  • 所需文件被打包到lib\pythonX.Y\*中,紧挨着嵌入式可执行文件或者在其上层目录(并且没有pyvenv.cfg来干扰搜索)。

  • 或者使用pyvenv.cfg将解释器指向正确的位置。


这是很多需要理解的内容。我不知道venvvirtualenv之间有(显著的)区别。当我在任何一个环境之外尝试这些步骤(假设模块在全局范围内可用,没有设置PYTHONPATH),它可以正常工作;这帮助我理解了一些事情。您的答案需要我花费一些时间来理解,感谢您花时间提供详细的答案! - r2evans
我知道嵌入式可执行文件仍需要“完整”的Python安装,但当我运行./hello时,尽管在可执行文件旁边有lib/python3.5/site-packages/Crypto,它仍然失败了。是否更安全地解释“嵌入式可执行文件旁边”是假定可执行文件必须在./lib/旁边的子目录中?(我之前没有意识到venv的这种微妙差别,但现在我看到了行为上的差异。) - r2evans
1
如果没有 pyvenv.cfg 文件干扰搜索,那么“exe”旁边就足够了(至少对于Python3.7-无法使用Python3.5进行测试),但是您的建议更安全(将exe放在子文件夹中),因为它也适用于存在 pyenv.cfg 的情况。 - ead

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接