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

2025-02-11 09:51:00
admin
原创
56
摘要:问题描述:这个setup.py:from distutils.core import setup from distutils.extension import Extension from Cython.Build import cythonize extensions = ( Extension...

问题描述:

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

知道如何让它工作吗?


解决方案 1:

首先,我应该指出,使用 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 + "\"
"

includesFile = open("src/extension-name.pyx", "w")
includesFile.write(includesContents)
includesFile.close()

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

我当然不会称其为一种优选的构建方法,但如果所有内容都必须放在一个扩展模块中,那么这是我能看到的唯一方法。

解决方案 2:

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

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


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

  • 与此类似,也与上面提到的答案类似,使用适当的功能扩展 Finders

  • 第二种解决方案是引入具有“正确”名称的符号链接,这将显示给公共模块(但这里拥有一个公共模块的优势在某种程度上被抵消了)。


初步说明:自 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初始化函数,但 Python 不知道在哪里查找并放弃搜索。PyInit_bar_a`foo.so`

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

导入模块时,Python 使用中的查找器sys.meta_path,它会返回模块的正确加载器(为简单起见,我使用带有加载器而不是module-spec 的传统工作流程)。默认查找器返回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

把它们放在一起:

文件夹结构:

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

*初始化.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")

设置.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)

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

解决方案 3:

为了清楚起见:这不是 Cython 真正支持的东西。Cython 的内部模型基于 Python 的“一个文件 = 一个模块”模型,并且不太可能改变。以下答案是一种 hack 解决方法,应如此处理。


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

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

import sys
import importlib
import importlib.abc

# 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_spec(self, fullname, path, target=None):
        if fullname.startswith(self.name_filter):
            # use this extension-file but PyInit-function of another module:
            loader = importlib.machinery.ExtensionFileLoader(fullname, __file__)
            return importlib.util.spec_from_loader(fullname, loader)
    
# 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的回答,我编写了一个工具,用于从 Python 包构建二进制 Cython 扩展。该包可以包含子包,这些子包也将包含在二进制文件中。这就是想法。

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

  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

这导致了第二个问题,我们无法使用原始方法从子包 (例如) 导入任何内容。通过引入执行重定向的查找器 (修改自@DavidW 的答案from foo.bar import base)解决了此问题。

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:

您还可以使用受此对话启发的名为 snakehouse 的库。

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

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用