这个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
这一事实。
知道如何让这个工作吗?
首先,我应该注意使用Cython编译带有子包的单个impossible文件是.so
。因此,如果你想要子包,你将不得不生成多个.so
文件,因为每个.so
只能代表一个模块。
其次,似乎你不能编译多个Cython / Python文件(我特意使用的是Cython语言)并将它们链接到一个模块中。
我试图用.so
和手动编译将多个Cython文件编译成单个distutils
,并且它总是无法在运行时导入。
似乎可以将已编译的Cython文件与其他库或甚至其他C文件链接起来,但在将两个已编译的Cython文件链接在一起时会出现问题,结果不是正确的Python扩展。
我能看到的唯一解决方案是将所有内容编译为单个Cython文件。在我的情况下,我编辑了我的setup.py
生成一个.pyx
文件,反过来include
s我的源目录中的每个.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
文件。
我当然不会称这是一个更好的构建方法,但如果一切都必须在一个扩展模块中,这是我能看到的唯一方法。
这个答案提供了Python3的原型(可以很容易地适应Python2),并展示了如何将几个cython模块捆绑到单个扩展/共享库/ pyd文件中。
我保留了它的历史/教学原因 - 给出了一个更简洁的配方in this answer,它提供了一个很好的替代@Mylin的建议,把所有东西都放在同一个pyx文件中。
初步说明:自Cython 0.29起,Cython使用Python> = 3.5的多阶段初始化。需要关闭多阶段初始化(否则PyInit_xxx
是不够的,请参阅此SO帖子),这可以通过将-DCYTHON_PEP489_MULTI_PHASE_INIT=0
传递给gcc /其他编译器来完成。
当将多个Cython-extension(让我们称之为bar_a
和bar_b
)捆绑到一个共享对象(让我们称之为foo
)时,主要问题是import bar_a
操作,因为模块的加载方式在Python中工作(显然简化):
bar_a.py
并加载它,如果不成功...bar_a.so
(或类似的),使用ldopen
加载共享库并调用PyInit_bar_a
来初始化/注册模块。现在,问题在于没有找到bar_a.so
,虽然初始化函数PyInit_bar_a
可以在foo.so
中找到,但Python不知道在哪里查找并放弃搜索。
幸运的是,有可用的钩子,所以我们可以教Python查找正确的位置。
导入模块时,Python使用来自finders的sys.meta_path
,它为模块返回正确的loader(为简单起见,我使用的是传统的工作流程与加载器而不是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
文件夹结构:
../
|-- setup.py
|-- foo/
|-- __init__.py
|-- bar_a.pyx
|-- bar_b.pyx
|-- bootstrap.pyx
__init__.朋友:
# 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()
vootstrap.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.朋友:
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的方法。
这个答案遵循@ 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
。