将多个子模块折叠为一个Cython扩展

Rei*_*ien 10 python distutils python-module cython

这个setup.py:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

extensions = (
    Extension('myext', ['myext/__init__.py',
                        'myext/algorithms/__init__.py',
                        'myext/algorithms/dumb.py',
                        'myext/algorithms/combine.py'])
)
setup(
    name='myext',
    ext_modules=cythonize(extensions)
)
Run Code Online (Sandbox Code Playgroud)

没有预期的效果.我想要它生产单一的myext.so,它做的; 但是当我通过它调用它时

python -m myext.so
Run Code Online (Sandbox Code Playgroud)

我明白了:

ValueError: Attempted relative import in non-package
Run Code Online (Sandbox Code Playgroud)

由于myext试图参考的事实.algorithms.

知道如何让这个工作吗?

lcm*_*lin 9

首先,我应该注意,使用Cython 编译带有子包的单个文件是不可能的.so.因此,如果您想要子包,则必须生成多个.so文件,因为每个文件.so只能代表一个模块.

其次,似乎你不能编译多个Cython/Python文件(我特意使用的是Cython语言)并将它们链接到一个模块中.

我尝试将多个Cython文件编译成一个单独的.so方式,无论是使用distutils还是手动编译,它总是无法在运行时导入.

似乎可以将已编译的Cython文件与其他库或甚至其他C文件链接起来,但在将两个已编译的Cython文件链接在一起时会出现问题,结果不是正确的Python扩展.

我能看到的唯一解决方案是将所有内容编译为单个Cython文件.在我的情况下,我编辑了我setup.py生成一个.pyx文件,而这个文件又include.pyx我的源目录中的每个文件:

includesContents = ""
for f in os.listdir("src-dir"):
    if f.endswith(".pyx"):
        includesContents += "include \"" + f + "\"\n"

includesFile = open("src/extension-name.pyx", "w")
includesFile.write(includesContents)
includesFile.close()
Run Code Online (Sandbox Code Playgroud)

然后我就编译了extension-name.pyx.当然,这会破坏增量和并行编译,并且由于所有内容都粘贴到同一个文件中,您最终可能会遇到额外的命名冲突.好的一面是,您不必编写任何.pyd文件.

我当然不会称这是一个更好的构建方法,但如果一切都必须在一个扩展模块中,这是我能看到的唯一方法.


Dav*_*idW 9

这个答案遵循@ead 答案的基本模式,但使用了一种稍微简单的方法,它消除了大部分样板代码。

唯一的区别是更简单的版本bootstrap.pyx

import sys
import importlib

# Chooses the right init function     
class CythonPackageMetaPathFinder(importlib.abc.MetaPathFinder):
    def __init__(self, name_filter):
        super(CythonPackageMetaPathFinder, self).__init__()
        self.name_filter =  name_filter

    def find_module(self, fullname, path):
        if fullname.startswith(self.name_filter):
            # use this extension-file but PyInit-function of another module:
            return importlib.machinery.ExtensionFileLoader(fullname,__file__)


# injecting custom finder/loaders into sys.meta_path:
def bootstrap_cython_submodules():
    sys.meta_path.append(CythonPackageMetaPathFinder('foo.')) 
Run Code Online (Sandbox Code Playgroud)

本质上,我会查看被导入模块的名称是否以 开头foo.,如果是,我会重用标准importlib方法来加载扩展模块,将当前.so文件名作为要查找的路径 - init 函数的正确名称(有多个)将从包名中推导出来。

显然,这只是一个原型——人们可能想要做一些改进。例如,现在import foo.bar_c会导致一个有点不寻常的错误消息:"ImportError: dynamic module does not define module export function (PyInit_bar_c)",可以返回None所有不在白名单上的子模块名称。


ead*_*ead 5

该答案提供了Python3的原型(可以轻松地适用于Python2),并说明了如何将多个cython模块捆绑到单个扩展名/共享库/ pyd文件中。

由于历史/教学上的原因,我将其保留- 在此答案中给出了更简洁的方法,它是@Mylin建议将所有内容都放入同一个pyx文件中的建议的一种很好的选择。


初步说明:从Cython 0.29开始,Cython对Python> = 3.5使用多阶段初始化。需要关闭多阶段初始化(否则PyInit_xxx还不够,请参见此SO-post),这可以通过传递-DCYTHON_PEP489_MULTI_PHASE_INIT=0给gcc /其他编译器来完成。


当将多个Cython扩展(让我们将它们bar_abar_b)捆绑到一个共享对象(让我们称之为foo)时,主要问题是import bar_a操作,因为模块的加载方式是在Python中进行的(显然,这很简单,此SO-post具有更多信息):

  1. 查找bar_a.so(或类似结果),ldopen用于加载共享库并调用PyInit_bar_a,如果不成功,则将初始化/注册模块
  2. 寻找bar_a.py并加载它,如果不成功的话...
  3. 查找bar_a.pyc并加载它(如果未成功-错误)。

步骤2和3.显然将失败。现在的问题是,找不到任何东西bar_a.so,尽管PyInit_bar_a可以在中找到初始化函数foo.so,Python不知道在哪里寻找并放弃了搜索。

幸运的是,有可用的钩子,因此我们可以教Python在正确的位置查找。

导入模块时,Python使用from中的查找器sys.meta_path,这些返回返回模块的正确加载器(为简单起见,我将传统的工作流程与加载器结合使用,而不是module-spec)。默认查找器返回None,即没有加载器,这将导致导入错误。

这意味着我们需要在上添加一个自定义查找器sys.meta_path,以识别我们捆绑的模块和返回加载器,这又将它们称为正确的PyInit_xxx功能。

缺少的部分:自定义查找器应如何找到它的路径sys.meta_path?如果用户必须手动执行操作,将非常不便。

导入包的子模块时,首先__init__.py会加载包的-module,这是我们可以注入自定义查找器的地方。

调用python setup.py build_ext install下面进一步介绍的设置后,将安装一个共享库,并且可以照常加载子模块:

>>> import foo.bar_a as a
>>> a.print_me()
I'm bar_a
>>> from foo.bar_b import print_me as b_print
>>> b_print()
I'm bar_b
Run Code Online (Sandbox Code Playgroud)

放在一起:

资料夹结构:

../
 |-- setup.py
 |-- foo/
      |-- __init__.py
      |-- bar_a.pyx
      |-- bar_b.pyx
      |-- bootstrap.pyx
Run Code Online (Sandbox Code Playgroud)

__init__.py

# bootstrap is the only module which 
# can be loaded with default Python-machinery
# because the resulting extension is called `bootstrap`:
from . import bootstrap

# injecting our finders into sys.meta_path
# after that all other submodules can be loaded
bootstrap.bootstrap_cython_submodules()
Run Code Online (Sandbox Code Playgroud)

bootstrap.pyx

import sys
import importlib

# custom loader is just a wrapper around the right init-function
class CythonPackageLoader(importlib.abc.Loader):
    def __init__(self, init_function):
        super(CythonPackageLoader, self).__init__()
        self.init_module = init_function

    def load_module(self, fullname):
        if fullname not in sys.modules:
            sys.modules[fullname] = self.init_module()
        return sys.modules[fullname]

# custom finder just maps the module name to init-function      
class CythonPackageMetaPathFinder(importlib.abc.MetaPathFinder):
    def __init__(self, init_dict):
        super(CythonPackageMetaPathFinder, self).__init__()
        self.init_dict=init_dict

    def find_module(self, fullname, path):
        try:
            return CythonPackageLoader(self.init_dict[fullname])
        except KeyError:
            return None

# making init-function from other modules accessible:
cdef extern from *:
    """
    PyObject *PyInit_bar_a(void);
    PyObject *PyInit_bar_b(void);
    """
    object PyInit_bar_a()
    object PyInit_bar_b()

# wrapping C-functions as Python-callables:
def init_module_bar_a():
    return PyInit_bar_a()

def init_module_bar_b():
    return PyInit_bar_b()


# injecting custom finder/loaders into sys.meta_path:
def bootstrap_cython_submodules():
    init_dict={"foo.bar_a" : init_module_bar_a,
               "foo.bar_b" : init_module_bar_b}
    sys.meta_path.append(CythonPackageMetaPathFinder(init_dict))  
Run Code Online (Sandbox Code Playgroud)

bar_a.pyx

def print_me():
    print("I'm bar_a")
Run Code Online (Sandbox Code Playgroud)

bar_b.pyx

def print_me():
    print("I'm bar_b")
Run Code Online (Sandbox Code Playgroud)

setup.py

from setuptools import setup, find_packages, Extension
from Cython.Build import cythonize

sourcefiles = ['foo/bootstrap.pyx', 'foo/bar_a.pyx', 'foo/bar_b.pyx']

extensions = cythonize(Extension(
            name="foo.bootstrap",
            sources = sourcefiles,
    ))


kwargs = {
      'name':'foo',
      'packages':find_packages(),
      'ext_modules':  extensions,
}


setup(**kwargs)
Run Code Online (Sandbox Code Playgroud)

注意:这个答案是我实验的起点,但是它使用了PyImport_AppendInittab,我看不到如何将其插入普通python中的方法。