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

Posted

技术标签:

【中文标题】将多个子模块折叠成一个 Cython 扩展【英文标题】:Collapse multiple submodules to one Cython extension 【发布时间】:2015-07-21 08:22:39 【问题描述】:

这个 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

知道如何让它工作吗?

【问题讨论】:

本指南对您有帮助吗? github.com/cython/cython/wiki/PackageHierarchy 我实际上已经阅读并遵循了它;问题是它们没有单个二进制输出。它们为每个 Python 文件生成一个二进制文件。 【参考方案1】:

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

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

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

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

我能看到的唯一解决方案是将所有内容编译为单个 Cython 文件。在我的情况下,我编辑了我的setup.py 以生成一个.pyx 文件,而includes 又在我的源目录中的每个.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 文件。

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

【讨论】:

【参考方案2】:

这个答案提供了 Python3 的原型(可以很容易地适应 Python2),并展示了如何将几个 cython 模块捆绑到单个扩展/共享库/pyd 文件中。

出于历史/教学原因,我保留它 - 提供了一个更简洁的配方 in this answer,它是 @Mylin 将所有内容放入同一个 pyx 文件的提议的一个很好的替代方案。


PEP489也讨论了同一个共享对象中多个模块的问题,这里提出了两种解决方案:

与此类似,并与 already above referred answer 类似,扩展了具有适当功能的查找器 第二种解决方案是引入具有“正确”名称的符号链接,这将显示给公共模块(但在这里,拥有一个公共模块的优势在某种程度上被否定了)。

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


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

    查找bar_a.so(或类似名称),使用ldopen 加载共享库并调用PyInit_bar_a,如果不成功,将初始化/注册模块 寻找bar_a.py并加载它,如果不成功... 查找 bar_a.pyc 并加载它,如果不成功 - 错误。

步骤 2. 和 3. 显然会失败。现在的问题是找不到bar_a.so,虽然PyInit_bar_a可以在foo.so中找到初始化函数foo.so,但Python不知道去哪里找,放弃了搜索。

幸运的是,有可用的钩子,所以我们可以教 Python 寻找正确的位置。

导入模块时,Python 使用来自sys.meta_path 的finders,它为模块返回正确的loader(为简单起见,我使用带有加载器的旧工作流,而不是module-spec)。默认查找器返回None,即没有加载程序,它会导致导入错误。

这意味着我们需要向sys.meta_path 添加一个自定义查找器,它会识别我们捆绑的模块并返回加载器,然后它们会调用正确的PyInit_xxx-function。

缺失的部分:自定义查找器应该如何进入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

###把它们放在一起:

文件夹结构:

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

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

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

bar_a.pyx

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

bar_b.pyx

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

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)

注意:This answer 是我实验的起点,但它使用了PyImport_AppendInittab,我看不出如何将它插入到普通的 python 中。

【讨论】:

昨天我自己也在考虑类似的方法。我想知道您是否可以使用现有的ExtensionFileLoader 而不是自己编写,因为模块名称和路径看起来可能不同。如果是这种情况,那么您也许可以稍微简化您的方法 @DavidW 感谢您的提示!有可能让它工作,并会大大减少必要的样板代码。但是由于我对机器的了解很差,我在修修补补了一段时间后无法让它工作...... 最终让它工作起来很容易。我已经制作了我的答案社区维基,因为这个答案完成了大部分工作 - 如果您只想将更改编辑到这个答案中(或保持原样),我很高兴【参考方案3】:

此答案遵循@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 方法来加载扩展模块,将当前.so 文件名作为要查看的路径 - init 函数的正确名称(有多个)将从包名称中推断出来。

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

【讨论】:

【参考方案4】:

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

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

    将整个包(包括所有子包)折叠到单个 Cython 扩展中 照常允许导入

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

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)

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

【讨论】:

【参考方案5】:

您也可以使用library inspired by this conversation 称为蛇屋。

完全披露:我是它的作者。 需要审核:此链接不会过期,因为它是LLC 拥有的永久 GitHub 链接

【讨论】:

我认为这可以通过一个简短的例子来改进。这绝对是图书馆应该完成的那种繁琐但重复的任务(所以链接到一个很有用)但是这里的答案预计会有更多的内容而不仅仅是一个链接

以上是关于将多个子模块折叠成一个 Cython 扩展的主要内容,如果未能解决你的问题,请参考以下文章

Weka java扩展子样本过滤器

scss YAML响应(扩展子列)

C++ 实现子类扩展子接口而不覆盖已经覆盖的方法

使用 Cython 时如何将一个 C++ 类(引用)传递给另一个?

如何将 Cython 生成的模块从 python 导入 C/C++ 主文件? (用 C/C++ 编程)[关闭]

cython与python的不同都有哪些