Свернуть несколько подмодулей на одно расширение 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)
)
Не имеет предполагаемого эффекта. Я хочу, чтобы он создал один myext.so
, который он делает; но когда я вызываю его через
python -m myext.so
Я получаю:
ValueError: Attempted relative import in non-package
из-за того, что myext
пытается ссылаться на .algorithms
.
Любая идея, как это сделать?
Ответы
Ответ 1
Прежде всего, я должен отметить, что невозможно скомпилировать один .so
файл с подпакетами, используя Cython. Поэтому, если вам нужны подпакеты, вам нужно сгенерировать несколько файлов .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()
Затем я просто компилирую extension-name.pyx
. Конечно, это нарушает пошаговую и параллельную компиляцию, и вы можете столкнуться с дополнительными конфликтами имен, так как все вставляется в один и тот же файл. С другой стороны, вам не нужно писать никаких файлов .pyd
.
Я, конечно, не назвал бы это предпочтительным методом сборки, но если все обязательно должно быть в одном модуле расширения, это единственный способ, которым я могу это сделать.
Ответ 2
Этот ответ предоставляет прототип для Python3 (который может быть легко адаптирован для Python2) и показывает, как несколько Cython -module могут быть объединены в одно расширение /shared-library/pyd-file.
Я храню его по историческим/дидактическим причинам - в этом ответе дается более сжатый рецепт, который представляет собой хорошую альтернативу предложению @Mylin поместить все в один и тот же pyx файл.
Предварительное примечание: Начиная с Cython 0.29, Cython использует многофазную инициализацию для Python> = 3.5. Необходимо отключить многофазную инициализацию (в противном случае PyInit_xxx
недостаточно, см. этот SO-post), что можно сделать, передав -DCYTHON_PEP489_MULTI_PHASE_INIT=0
компилятору gcc/other.
При объединении нескольких расширений Cython (пусть они называются bar_a
и bar_b
) в один общий объект (пусть его называют foo
), основной проблемой является операция import bar_a
из-за способа загрузки модулей работает на Python (очевидно, упрощенно, эта SO-запись содержит больше информации):
- Найдите
bar_a.so
(или аналогичный), используйте ldopen
для загрузки общей библиотеки и вызовите PyInit_bar_a
, который инициализирует/зарегистрирует модуль, если не удастся
- Ищите
bar_a.py
и загружайте его, если не удалось...
- Найдите
bar_a.pyc
и загрузите его, если не удалось - ошибка.
Шаги 2. и 3. очевидно потерпят неудачу. Теперь проблема в том, что нет bar_a.so
, который можно найти, и хотя функцию инициализации PyInit_bar_a
можно найти в foo.so
, Python не знает, где искать и бросает поиск.
К счастью, есть доступные хуки, поэтому мы можем научить Python искать нужные места.
При импорте модуля Python использует искатели из sys.meta_path
, которые возвращают правильный загрузчик для модуля (для простоты я использую устаревший рабочий процесс с загрузчики, а не 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)
NB: Этот ответ послужил отправной точкой для моих экспериментов, однако он использует PyImport_AppendInittab
, и я не вижу способа, как это можно подключить к обычному питону.
Ответ 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
для всех имен подмодулей, которых нет в белом списке.