如何扩展 Python 并制作 C 包?

How to extend Python and make a C-package?

我不久前在我的 C 应用程序中嵌入并扩展了 Python 2.7。在火车上晚点我把它带到 Python 3,模块注册的很多初始化对我来说都改变了。

在我使用 PyModule_Create 创建模块并在之后添加成员之前,甚至添加子模块以便我可以执行:

from foo.bar import bas

I added/appended 'top-level' 模块到 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 也可以创建包含子模块的包吗?

非常感谢!

如果没有最小的可重现示例,很难说出哪里出了问题以及您在答案中具体寻找什么。不过,我会尝试提供一些帮助。

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 传递关键字参数。

希望对您有所帮助。

我不知道你在这里尝试拉的东西(嵌套扩展模块)是否 OK,无论如何,推荐的结构代码方式是通过 [Python 3.Docs]: Modules - Packages.
但是,我将此(重现问题,解决问题)作为个人练习。

1。简介

列出 2 个相关页面:

环境:

[cfati@CFATI-5510-0:e:\Work\Dev\Whosebug\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。 Python2

虚拟模块试图重现问题中提到的行为。

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\Whosebug\q061692747\py2]> sopr.bat
*** Set shorter prompt to better fit when pasted in Whosebug (or other) pages ***

[prompt]> "f:\Install\pc032\Microsoft\VisualCForPython208\Microsoft\Visual C++ for Python.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.07.17\include" mod.c  /link /NOLOGO /DLL /OUT:mod.pyd /LIBPATH:"c:\Install\pc064\Python\Python.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。 Python3

转换为 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\Whosebug\q061692747\py3]> sopr.bat
*** Set shorter prompt to better fit when pasted in Whosebug (or other) pages ***

[prompt]> "c:\Install\pc032\Microsoft\VisualStudioCommunity17\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.07.06\include" mod.c  /link /NOLOGO /DLL /OUT:mod.pyd /LIBPATH:"c:\Install\pc064\Python\Python.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\Whosebug\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 中。作为一般化,"nested" 扩展子模块不再通过包含它们的初始化函数的模块导入。唯一的选择是手动导入它们。
注意:我认为这个Python 3限制是有原因的,所以下面的内容就像在玩火.

mod.c.

中的两行注释掉

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.07.06\include" mod.c  /link /NOLOGO /DLL /OUT:mod.pyd /LIBPATH:"c:\Install\pc064\Python\Python.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 函数return 个代码。这可能会导致难以发现错误(甚至崩溃)并且 永远不应该这样做 (尤其是在生产代码中)。

我不太确定 PyImport_ExtendInittab 效果到底是什么,因为我没有玩过它,但是 [Python 3.Docs]: Importing Modules - int PyImport_ExtendInittab(struct _inittab *newtab) 说(重点是我的):

This should be called before Py_Initialize().

因此,在我们的上下文中调用它是不可能的。

也提到这个(旧的)讨论(不确定它是否包含相关信息,但仍然)[Python.Mail]: [Python-Dev] nested extension modules?

这个答案我晚了一年,但是,偶然发现了与 OP 相同的问题,我相信我找到了比公认答案更清晰的解决方案。

我只介绍 Python 3,因为这是 OP 希望解决的问题,而且现在是 2021 年。

问题

内置模块虽然遵循与扩展模块相同的约定,但不会被编译到共享库中并作为文件分发——在 embedding [= 时这样做更有意义92=] 进入更大的应用程序,因为通用 Python 应用程序或交互式解释器不应访问该模块。

正如 OP 所发现的那样,使用 PyImport_ExtendInittab 向解释器注册了一个内置模块。但是,如果名称是 nested(例如 foo.bar.bas,而不是 bas),则默认导入机制将不起作用。

已接受答案存在问题

接受的答案加载模块并在它注册到解释器后立即执行(即当调用 PyMODINIT_FUNC 函数时)。随后从 Python 导入模块将简单地 return 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)通过使用其父包的__path__属性调用find_spec方法来查找第二个参数(即 find_spec('foo.bar.bas', foo.bar.__path__).

这可以通过设置纯 Python 父包(例如 Python 路径中的 foo/bar/__init__.py 来轻松测试:

__path__ = None

然后可以导入名为 foo.bar.bas 并通过 PyImport_ExtendInittab 注册的内置扩展模块。

这种行为有点documented:

Some meta path finders only support top level imports. These importers will always return None when anything other than None is passed as the second argument.

解决方案

上面的测试有点 hack,它依赖于对实现细节的了解,无论如何,只有在 foo.bar 下面不需要非内置模块的情况下,才能将其视为一种解决方案 – a在这种情况下,名为 foo.bar.moo 的纯 Python 模块(即在 foo/bar/moo.py 中定义)将无法导入。

一个更简洁的解决方案是定义一个 MetaPathFinder,这似乎也是 encouraged:

The most reliable mechanism for replacing the entire import system is to delete the default contents of sys.meta_path, replacing them entirely with a custom meta path hook.

当然,我们可以保留现有的MetaPathFinder,简单地扩展列表。 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,从而为其内容提供更大的灵活性。