Компиляция необязательного расширения Cython только по возможности в setup.py

У меня есть модуль Python полностью реализован на Python. (По причинам переносимости.)

Реализация небольшой части была продублирована в модуле Cython. Для улучшения производительности, где это возможно.

Я знаю, как установить .c модули, созданные Cython с distutils, Однако, если на машине не установлен компилятор, я подозреваю, что установка не удастся, хотя модуль все еще можно использовать в режиме чистого Python.

Есть ли способ скомпилировать .c модуль, если возможно, но изящно выйдет из строя и установить без него, если компиляция невозможна?

2 ответа

Я думаю, вам придется внести некоторые изменения как в вашем setup.py и в одном __init__ файл в вашем модуле.

Допустим, имя вашего пакета будет "модуль" и у вас есть функциональность, sub для которого у вас есть чистый код Python в sub подпапка и эквивалентный код C в c_sub вложенная папка. Например, в вашем setup.py:

import logging
from setuptools.extension import Extension
from setuptools.command.build_ext import build_ext
from distutils.errors import CCompilerError, DistutilsExecError, DistutilsPlatformError

logging.basicConfig()
log = logging.getLogger(__file__)

ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError, IOError)

class BuildFailed(Exception):
    pass

def construct_build_ext(build_ext):
    class WrappedBuildExt(build_ext):
        # This class allows C extension building to fail.
        def run(self):
            try:
                build_ext.run(self)
            except DistutilsPlatformError as x:
                raise BuildFailed(x)

        def build_extension(self, ext):
            try:
                build_ext.build_extension(self, ext)
            except ext_errors as x:
                raise BuildFailed(x)
    return WrappedBuildExt

setup_args = {'name': 'module', 'license': 'BSD', 'author': 'xxx',
    'packages': ['module', 'module.sub', 'module.c_sub'],
    'cmdclass': {}
    }

ext_modules = [Extension("module.c_sub._sub", ["module/c_sub/_sub.c"])]
cmd_classes = setup_args.setdefault('cmdclass', {})

try:
    # try building with c code :
    setup_args['cmdclass']['build_ext'] = construct_build_ext(build_ext)
    setup(ext_modules=ext_modules, **setup_args)
except BuildFailed as ex:
    log.warn(ex)
    log.warn("The C extension could not be compiled")

    ## Retry to install the module without C extensions :
    # Remove any previously defined build_ext command class.
    if 'build_ext' in setup_args['cmdclass']:
        del setup_args['cmdclass']['build_ext']
    if 'build_ext' in cmd_classes:
        del cmd_classes['build_ext']

    # If this new 'setup' call don't fail, the module 
    # will be successfully installed, without the C extension :
    setup(**setup_args)
    log.info("Plain-Python installation succeeded.")

Теперь вам нужно будет включить что-то подобное в свой __init__.py файл (или в любом месте, соответствующем вашему делу):

try:
    from .c_sub import *
except ImportError:
    from .sub import *

Таким образом, будет использоваться версия C, если она была собрана, в противном случае используется простая версия Python. Предполагается, что sub а также c_sub предоставит тот же API.

Вы можете найти пример установочного файла, делающего это в Shapely пакет. На самом деле большая часть кода, который я разместил, была скопирована (construct_build_ext функция) или адаптированы (строки после) из этого файла.

Класс Extension имеет параметр optional в конструкторе:

необязательный - указывает, что сбой сборки в расширении не должен прерывать процесс сборки, а просто пропускать расширение.

Здесь также ссылка на довольно интересную историю кусок кода, предложенного mgc.

Вопрос Как мне структурировать пакет Python, содержащий код Cython

связано, там вопрос, как перейти от Cython к "уже сгенерированному C-коду". Вы можете использовать аналогичную стратегию, чтобы выбрать, какой из .py или .pyx код для установки.

Другие вопросы по тегам