如何扩展 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 不支持隐式相对导入而不是模块初始化。 您必须提供更多详细信息。 foobarbas 是什么? 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 的默认内容,将它们完全替换为自定义元路径挂钩。

当然,我们可以保留现有的MetaPathFinders,只需扩展列表即可。 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 包?的主要内容,如果未能解决你的问题,请参考以下文章

我应该包括啥来制作 boost.python 扩展?

python 制作自定义包并安装到系统目录

linux 下的动态库制作 以及在python 中如何调用 c 函数库

Google PlayAPK 扩展包 ( 2021年09月02日最新处理方案 | 制作 APK 扩展包 | 上传 APK 扩展包到 Google Play | APK 扩展文件上传时机 )

我应该如何构建包含 Cython 代码的 Python 包

如何制作可扩展的 UITableView 标头?