如何扩展 Python 并制作 C 包?
Posted
技术标签:
【中文标题】如何扩展 Python 并制作 C 包?【英文标题】:How to extend Python and make a C-package? 【发布时间】:2020-05-09 06:32:08 【问题描述】:不久前,我在我的 C 应用程序中嵌入并扩展了 Python 2.7。在火车的后期,我将它带到了 Python 3,并且模块注册的许多初始化对我来说都发生了变化。
在我使用PyModule_Create
创建模块之前,然后添加成员,甚至是子模块,以便我可以执行:
from foo.bar import bas
我将“***”模块添加/附加到PyEval_GetBuiltins()
,这在 Py 2 中可能是错误的,但它有效。现在在 Py 3 中,我在上面的代码中收到了这个异常:
Traceback (most recent call last):
File "foo.py", line 1, in <module>
ModuleNotFoundError: No module named 'foo.bar'; 'foo' is not a package
查看文档,我现在找到了一个带有 PyImport_ExtendInittab
的示例。我对此有两个问题:
1) Inittab
应该是什么意思?文档说明了它的含义,但这个命名有点令人讨厌。什么是Inittab
?不应该叫PyImport_ExtendBuiltins
,这样我就明白了。
2) 我只能找到添加普通模块的示例。 PyImport_ExtendInittab
也可以创建带有子模块的包吗?
非常感谢!
【问题讨论】:
一个最小的例子(即minimal reproducible example)将有助于理解你到底在做什么(错误)。您的问题可能是由于 Python3 不支持隐式相对导入而不是模块初始化。 您必须提供更多详细信息。 foo、bar、bas 是什么? Python 2 中的模块包树看起来如何? (简化的)源代码是什么样子的? 请分享模块代码(或至少一个函数和初始化部分),以及如何从 Python 2 调用它的示例。 【参考方案1】:我不知道你想在这里提取什么(嵌套扩展模块)是否OK,无论如何推荐的结构化代码方式是通过[Python 3.Docs]: Modules - Packages。 但是,我将这个(重现问题、修复问题)作为个人练习。
1。简介
列出 2 个相关页面:
[Python 3.Docs]: Module Objects [Python 2.Docs]: Module Objects环境:
[cfati@CFATI-5510-0:e:\Work\Dev\***\q061692747]> tree /a /f Folder PATH listing for volume SSD0-WORK Volume serial number is AE9E-72AC E:. | test00.py | +---py2 | mod.c | \---py3 helper.c mod.c
2。 Python 2
虚拟模块试图重现问题中提到的行为。
mod.c:
#include <stdio.h>
#include <Python.h>
#define MOD_NAME "mod"
#define SUBMOD_NAME "submod"
static PyObject *pMod = NULL;
static PyObject *pSubMod = NULL;
static PyMethodDef modMethods[] =
NULL
;
PyMODINIT_FUNC initmod()
if (!pMod)
pMod = Py_InitModule(MOD_NAME, modMethods);
if (pMod)
PyModule_AddIntConstant(pMod, "i", -69);
pSubMod = Py_InitModule(MOD_NAME "." SUBMOD_NAME, modMethods);
if (pSubMod)
PyModule_AddStringConstant(pSubMod, "s", "dummy");
if (PyModule_AddObject(pMod, SUBMOD_NAME, pSubMod) < 0)
Py_XDECREF(pMod);
Py_XDECREF(pSubMod);
return;
输出:
[cfati@CFATI-5510-0:e:\Work\Dev\***\q061692747\py2]> sopr.bat *** Set shorter prompt to better fit when pasted in *** (or other) pages *** [prompt]> "f:\Install\pc032\Microsoft\VisualCForPython2\2008\Microsoft\Visual C++ for Python\9.0\vcvarsall.bat" x64 Setting environment for using Microsoft Visual Studio 2008 x64 tools. [prompt]> dir /b mod.c [prompt]> cl /nologo /MD /DDLL /I"c:\Install\pc064\Python\Python\02.07.17\include" mod.c /link /NOLOGO /DLL /OUT:mod.pyd /LIBPATH:"c:\Install\pc064\Python\Python\02.07.17\libs" mod.c Creating library mod.lib and object mod.exp [prompt]> dir /b mod.c mod.exp mod.lib mod.obj mod.pyd mod.pyd.manifest [prompt]> "e:\Work\Dev\VEnvs\py_pc064_02.07.17_test0\Scripts\python.exe" Python 2.7.17 (v2.7.17:c2f86d86e6, Oct 19 2019, 21:01:17) [MSC v.1500 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> >>> [item for item in sys.modules if "mod" in item] [] >>> import mod >>> >>> [item for item in sys.modules if "mod" in item] # !!! NOTICE the contents !!! ['mod.submod', 'mod'] >>> >>> mod <module 'mod' from 'mod.pyd'> >>> mod.i -69 >>> mod.submod <module 'mod.submod' (built-in)> >>> mod.submod.s 'dummy' >>> >>> from mod.submod import s >>> s 'dummy' >>>
如所见,导入带有子模块的模块,在 sys.path 中添加子模块(没看过,但我 99.99% 确定这是由 Py_InitModule)
3。 Python 3
转换为 Python 3。由于这是第 1st 步骤,请将 2 条注释行视为不存在。
mod.c:
#include <stdio.h>
#include <Python.h>
//#include "helper.c"
#define MOD_NAME "mod"
#define SUBMOD_NAME "submod"
static PyObject *pMod = NULL;
static PyObject *pSubMod = NULL;
static PyMethodDef modMethods[] =
NULL
;
static struct PyModuleDef modDef =
PyModuleDef_HEAD_INIT, MOD_NAME, NULL, -1, modMethods,
;
static struct PyModuleDef subModDef =
PyModuleDef_HEAD_INIT, MOD_NAME "." SUBMOD_NAME, NULL, -1, modMethods,
;
PyMODINIT_FUNC PyInit_mod()
if (!pMod)
pMod = PyModule_Create(&modDef);
if (pMod)
PyModule_AddIntConstant(pMod, "i", -69);
pSubMod = PyModule_Create(&subModDef);
if (pSubMod)
PyModule_AddStringConstant(pSubMod, "s", "dummy");
if (PyModule_AddObject(pMod, SUBMOD_NAME, pSubMod) < 0)
Py_XDECREF(pMod);
Py_XDECREF(pSubMod);
return NULL;
//addToSysModules(MOD_NAME "." SUBMOD_NAME, pSubMod);
return pMod;
输出:
[cfati@CFATI-5510-0:e:\Work\Dev\***\q061692747\py3]> sopr.bat *** Set shorter prompt to better fit when pasted in *** (or other) pages *** [prompt]> "c:\Install\pc032\Microsoft\VisualStudioCommunity\2017\VC\Auxiliary\Build\vcvarsall.bat" x64 ********************************************************************** ** Visual Studio 2017 Developer Command Prompt v15.9.23 ** Copyright (c) 2017 Microsoft Corporation ********************************************************************** [vcvarsall.bat] Environment initialized for: 'x64' [prompt]> dir /b helper.c mod.c [prompt]> cl /nologo /MD /DDLL /I"c:\Install\pc064\Python\Python\03.07.06\include" mod.c /link /NOLOGO /DLL /OUT:mod.pyd /LIBPATH:"c:\Install\pc064\Python\Python\03.07.06\libs" mod.c Creating library mod.lib and object mod.exp [prompt]> dir /b helper.c mod.c mod.exp mod.lib mod.obj mod.pyd [prompt]> "e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import sys >>> >>> [item for item in sys.modules if "mod" in item] [] >>> import mod >>> >>> [item for item in sys.modules if "mod" in item] # !!! NOTICE the contents !!! ['mod'] >>> >>> mod <module 'mod' from 'e:\\Work\\Dev\\***\\q061692747\\py3\\mod.pyd'> >>> mod.i -69 >>> mod.submod <module 'mod.submod'> >>> mod.submod.s 'dummy' >>> >>> from mod.submod import s Traceback (most recent call last): File "<stdin>", line 1, in <module> ModuleNotFoundError: No module named 'mod.submod'; 'mod' is not a package >>> ^Z [prompt]>
正如所见,嵌套导入是不可能的。这是因为 mod.submod 不存在于 sys.modules 中。作为概括,“嵌套”扩展子模块不再可以通过包含它们的初始化函数的模块导入。唯一的选择是手动导入它们。 作为说明:我认为 Python 3 的限制是有原因的,所以下面的内容就像玩火。
从 mod.c 中删除 2 行。
helper.c:
int addToSysModules(const char *pName, PyObject *pMod)
PyObject *pSysModules = PySys_GetObject("modules");
if (!PyDict_Check(pSysModules))
return -1;
PyObject *pKey = PyUnicode_FromString(pName);
if (!pKey)
return -2;
if (PyDict_Contains(pSysModules, pKey))
Py_XDECREF(pKey);
return -3;
Py_XDECREF(pKey);
if (PyDict_SetItemString(pSysModules, pName, pMod) == -1)
return -4;
return 0;
输出:
[prompt]> cl /nologo /MD /DDLL /I"c:\Install\pc064\Python\Python\03.07.06\include" mod.c /link /NOLOGO /DLL /OUT:mod.pyd /LIBPATH:"c:\Install\pc064\Python\Python\03.07.06\libs" mod.c Creating library mod.lib and object mod.exp [prompt]> "e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> >>> import sys >>> >>> [item for item in sys.modules if "mod" in item] [] >>> import mod >>> >>> [item for item in sys.modules if "mod" in item] # !!! NOTICE the contents :) !!! ['mod.submod', 'mod'] >>> >>> from mod.submod import s >>> s 'dummy' >>>
4。结束语
如上所述,这似乎更像是一种解决方法。更简洁的解决方案是通过包更好地组织模块。
由于这是出于演示目的,并且为了使代码尽可能简单,我并不总是检查 Python C API 函数的返回代码。这可能导致难以发现错误(甚至崩溃)并且永远不应该这样做(尤其是在生产代码中)。
我不太确定 PyImport_ExtendInittab 效果到底是什么,因为我没有玩过它,但 [Python 3.Docs]: Importing Modules - int PyImport_ExtendInittab(struct _inittab *newtab) 声明(强调是我的):
这应该在Py_Initialize()之前调用。
因此,在我们的上下文中调用它是不可能的。
还提到了这个(旧的)讨论(不确定它是否包含相关信息,但仍然)[Python.Mail]: [Python-Dev] nested extension modules?。
【讨论】:
非常感谢您的见解!这绝对澄清了很多!我使用了您的示例,但没有使用助手,而是将其与PyImport_GetModuleDict
结合使用,并且有效。再次,非常感谢!【参考方案2】:
如果没有一个可重复的最小示例,就很难说出哪里出了问题,以及您在答案中具体要寻找什么。不过,我会尽力提供一些帮助。
from foo.bar import bas
要使上述操作生效,您需要一个名为 foo 的文件夹中的文件 bar.py,并且 bar.py 必须包含一个功能bas()
。此外,文件夹 foo 必须包含一个空的 __init__.py 文件。
现在,如果您想在某处调用已编译的 C 文件,那么完成此操作的最简单方法可能是使用 os.system()
或 subprocess.call()
并像从命令行调用它一样调用该文件。
假设make文件在同一个目录:
import os
import subprocess
os.system("make run")
# or
subprocess.run("make run".split())
make run
根据需要运行您的 C 文件(在您的 makefile 中声明)。也可以随意使用 python f-strings 传递关键字参数。
希望这会有所帮助。
【讨论】:
这个答案是错误的有几个原因:1. OP 知道如何构建他们的代码,2. 通过 Python 构建代码 -> make -> ... 没有意义,而且很丑, 3. OP 想要导入构建的代码。 -1.【参考方案3】:这个答案我迟到了一年,但是,偶然发现了与 OP 相同的问题,我相信我找到了比公认答案更清洁的解决方案。
我只会介绍 Python 3,因为这是 OP 想要解决的问题,而且,现在是 2021 年。
问题
内置模块虽然遵循与扩展模块相同的约定,但不会编译为共享库并作为文件分发 - 当embedding Python 进入更大的应用程序时,这样做更有意义,因为通用 Python 应用程序或交互式解释器不应访问该模块。
一个内置模块使用PyImport_ExtendInittab
注册到解释器,正如 OP 发现的那样。但是,如果名称是 nested(例如 foo.bar.bas
,而不是 bas
),则默认导入机制将不起作用。
已接受答案的问题
接受的答案加载模块并在它注册到解释器后立即执行它(即当调用PyMODINIT_FUNC
函数时)。随后从 Python 导入模块只会返回 sys.modules
中的对象。
此外,这不适用于较新的(和推荐的)Multi-Phase Initialization,这会影响重新加载模块和使用子解释器的能力。
问题的原因
Python 导入机制非常好documented。任何导入的模块(无论是共享库支持的扩展、内置并通过 PyImport_ExtendInittab
注册还是纯 Python)都需要通过在 sys.meta_path
中注册的 MetaPathFinder
定位。默认情况下,内置模块位于importlib.machinery.BuiltinImporter
(恰好也是一个Loader)。但是,它的find_spec
方法定义为:
@classmethod
def find_spec(cls, fullname, path=None, target=None):
if path is not None:
return None
if _imp.is_builtin(fullname):
return spec_from_loader(fullname, cls, origin=cls._ORIGIN)
else:
return None
嵌套模块(例如foo.bar.bas
)是通过调用find_spec
方法来查找的,使用其父包的__path__
属性作为第二个参数(即find_spec('foo.bar.bas', foo.bar.__path__)
。
这可以通过设置纯 Python 父包(例如 Python 路径中的 foo/bar/__init__.py
)轻松测试:
__path__ = None
一个名为 foo.bar.bas
并通过 PyImport_ExtendInittab
注册的内置扩展模块将可以导入。
这种行为有点documented:
一些元路径查找器仅支持***导入。当 None 以外的任何内容作为第二个参数传递时,这些导入器将始终返回 None。
解决方案
上面的测试有点像 hack,它取决于对实现细节的了解,无论如何,如果在 foo.bar
下不需要非内置模块,则只能被视为一种解决方案——一个名为的纯 Python 模块在这种情况下,foo.bar.moo
(即在foo/bar/moo.py
中定义)将无法导入。
一个更简洁的解决方案是定义一个MetaPathFinder
,它似乎也是encouraged:
替换整个导入系统最可靠的机制是删除 sys.meta_path 的默认内容,将它们完全替换为自定义元路径挂钩。
当然,我们可以保留现有的MetaPathFinder
s,只需扩展列表即可。 foo/bar/__init__.py
中定义的以下代码(在撰写本文时仅依赖于文档化和未弃用的 API)可以解决问题:
import importlib.abc
import importlib.machinery
import importlib.util
import sys
class CustomBuiltinImporter(importlib.abc.MetaPathFinder):
_ORIGIN = 'custom-builtin'
@classmethod
def find_spec(cls, fullname, path, target=None):
if path != __path__ or not fullname.startswith(cls.__module__ + '.'):
return None
if fullname not in sys.builtin_module_names:
return None
return importlib.util.spec_from_loader(fullname, importlib.machinery.BuiltinImporter, origin=cls._ORIGIN)
sys.meta_path.append(CustomBuiltinImporter)
此代码不允许加载在 foo.bar
以外的任何内容下定义的内置模块。当然,自定义MetaPathFinder
可以在任何地方定义(包括在应用程序的一些引导代码中),但是find_spec
方法的第一次测试需要调整。这样的实现还允许foo.bar
成为namespace package,从而为其内容提供更大的灵活性。
【讨论】:
以上是关于如何扩展 Python 并制作 C 包?的主要内容,如果未能解决你的问题,请参考以下文章
linux 下的动态库制作 以及在python 中如何调用 c 函数库
Google PlayAPK 扩展包 ( 2021年09月02日最新处理方案 | 制作 APK 扩展包 | 上传 APK 扩展包到 Google Play | APK 扩展文件上传时机 )