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

时间:2015-05-10 22:38:06

标签: python cython python-module distutils

此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)
)

没有预期的效果。我希望它能产生一个myext.so,它会这样做;但是当我通过

调用它时
python -m myext.so

我明白了:

ValueError: Attempted relative import in non-package

由于myext尝试引用.algorithms

知道如何让这个工作吗?

4 个答案:

答案 0 :(得分:8)

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

其次,您似乎无法编译多个Cython / Python文件(我专门使用Cython语言)并将它们链接到一个模块中。

我尝试使用.so和手动编译将多个Cython文件编译成单个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()

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

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

答案 1 :(得分:3)

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

出于历史/教学上的原因,我将其保留-给出了更简洁的方法in this answer,它是@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)中时,主要问题是{{1} }操作,因为模块的加载方式是在Python中进行的(显然已简化):

  1. 寻找import bar_a并加载它,如果失败的话...
  2. 寻找bar_a.py(或类似名称),使用bar_a.so加载共享库并调用ldopen,这将初始化/注册模块。

现在,问题是找不到PyInit_bar_a,尽管可以在bar_a.so中找到初始化函数PyInit_bar_a,但是Python不知道在哪里寻找并给出进行搜索。

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

在导入模块时,Python利用finders中的sys.meta_path来返回模块的正确loader(为简单起见,我使用的是带有加载程序的旧工作流程,而不是{{ 3}})。默认查找器返回foo.so,即不加载程序,并导致导入错误。

这意味着我们需要向None添加一个自定义查找器,以识别我们捆绑的模块和返回加载器,这又将调用正确的sys.meta_path函数。

缺少的部分:自定义查找程序应如何进入PyInit_xxx?如果用户必须手动执行操作,将非常不便。

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

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

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

__ init __。py

../
 |-- setup.py
 |-- foo/
      |-- __init__.py
      |-- bar_a.pyx
      |-- bar_b.pyx
      |-- bootstrap.pyx

bootstrap.pyx

# 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()

bar_a.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))  

bar_b.pyx

def print_me():
    print("I'm bar_a")

setup.py

def print_me():
    print("I'm bar_b")

注意:module-spec是我实验的起点,但是它使用了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) ,我看不到如何将其插入普通python中的方法。

答案 2 :(得分:1)

此答案遵循@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.')) 

从本质上讲,我希望查看导入的模块的名称是否以foo.开头,如果确实如此,我将重用标准的importlib方法来加载扩展模块,并传递当前的{{ 1}}文件名作为查找路径-将从包名称中推断出init函数的正确名称(存在多个)。

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

答案 3 :(得分:0)

我编写了一个 tool 来从 Python 包构建二进制 Cython 扩展,基于上面 @DavidW @ead 的答案。该包可以包含子包,这些子包也将包含在二进制文件中。这是想法。

这里有两个问题需要解决:

  1. 将整个包(包括所有子包)折叠为单个 Cython 扩展
  2. 像往常一样允许导入

以上答案在单层布局上效果很好,但是当我们尝试进一步使用子包时,当不同子包中的任何两个模块具有相同名称时,就会出现名称冲突。例如,

foo/
  |- bar/
  |  |- __init__.py
  |  |- base.py
  |- baz/
  |  |- __init__.py
  |  |- base.py

会在生成的 C 代码中引入两个 PyInit_base 函数,导致函数定义重复。

此工具通过在构建之前将所有模块展平到根包层(例如 foo/bar/base.py -> foo/bar_base.py)来解决此问题。

这导致了第二个问题,我们不能使用原来的方式从子包中导入任何东西(例如from foo.bar import base)。这个问题是通过引入一个执行重定向的查找器(从 @DavidW's answer 修改而来)来解决的。

class _ExtensionLoader(_imp_mac.ExtensionFileLoader):
  def __init__(self, name, path, is_package=False, sep="_"):
    super(_ExtensionLoader, self).__init__(name, path)
    self._sep = sep
    self._is_package = is_package

  def create_module(self, spec):
    s = _copy.copy(spec)
    s.name = _rename(s.name, sep=self._sep)
    return super(_ExtensionLoader, self).create_module(s)

  def is_package(self, fullname):
    return self._is_package

# Chooses the right init function
class _CythonPackageMetaPathFinder(_imp_abc.MetaPathFinder):
  def __init__(self, name, packages=None, sep="_"):
    super(_CythonPackageMetaPathFinder, self).__init__()
    self._prefix = name + "."
    self._sep = sep
    self._start = len(self._prefix)
    self._packages = set(packages or set())

  def __eq__(self, other):
    return (self.__class__.__name__ == other.__class__.__name__ and
            self._prefix == getattr(other, "_prefix", None) and
            self._sep == getattr(other, "_sep", None) and
            self._packages == getattr(other, "_packages", None))

  def __hash__(self):
    return (hash(self.__class__.__name__) ^
            hash(self._prefix) ^
            hash(self._sep) ^
            hash("".join(sorted(self._packages))))

  def find_spec(self, fullname, path, target=None):
    if fullname.startswith(self._prefix):
      name = _rename(fullname, sep=self._sep)
      is_package = fullname in self._packages
      loader = _ExtensionLoader(name, __file__, is_package=is_package)
      return _imp_util.spec_from_loader(
          name, loader, origin=__file__, is_package=is_package)

它将原始导入(虚线)路径更改为其移动模块的相应位置。必须为加载程序提供一组子包,以将其作为包而不是非包模块加载。