我应该如何构造一个包含 Cython 代码的 Python 包

我想制作一个包含一些 Cython代码的 Python 包。我已经让 Cython 代码运行得很好了。然而,现在我想知道如何最好地包装它。

对于大多数只想安装软件包的人来说,我希望包含 Cython 创建的 .c文件,并安排 setup.py编译它来生成模块。然后用户就不需要安装 Cython 来安装软件包了。

但是对于那些可能想要修改软件包的人,我还想提供 Cython .pyx文件,并且以某种方式允许 setup.py使用 Cython 构建它们(因此那些用户需要安装 Cython)。

我应该如何构造包中的文件来满足这两种情况?

但是它没有说明如何制作一个单独的 setup.py来处理有或没有 Cython 的情况。

29422 次浏览

最简单的方法是同时包含这两个文件,但只使用 c 文件?包括。Pyx 文件很好,但是一旦您拥有了。C 文件。需要重新编译。Pyx 可以安装 Pyrex 并手动完成。

否则,您需要为首先构建 C 文件的 distutils 提供一个自定义 build _ ext 命令。Cython 已经包含了一个。http://docs.cython.org/src/userguide/source_files_and_compilation.html

这个文档没有说明如何使这个条件,但是

try:
from Cython.distutils import build_ext
except ImportError:
from distutils.command import build_ext

应该能搞定。

我现在已经在一个 Python 包 simplerandom(BitBucket 回购-EDIT: now Github)中自己完成了这项工作(我并不期望这是一个受欢迎的包,但这是一个学习 Cython 的好机会)。

这种方法依赖于这样一个事实: 使用 Cython.Distutils.build_ext(至少是 Cython 版本0.14)构建 .pyx文件似乎总是在与源 .pyx文件相同的目录中创建 .c文件。

下面是 setup.py的一个缩减版,我希望它能展示一些要点:

from distutils.core import setup
from distutils.extension import Extension


try:
from Cython.Distutils import build_ext
except ImportError:
use_cython = False
else:
use_cython = True


cmdclass = {}
ext_modules = []


if use_cython:
ext_modules += [
Extension("mypackage.mycythonmodule", ["cython/mycythonmodule.pyx"]),
]
cmdclass.update({'build_ext': build_ext})
else:
ext_modules += [
Extension("mypackage.mycythonmodule", ["cython/mycythonmodule.c"]),
]


setup(
name='mypackage',
...
cmdclass=cmdclass,
ext_modules=ext_modules,
...
)

我还编辑了 MANIFEST.in,以确保 mycythonmodule.c包含在源发行版(用 python setup.py sdist创建的源发行版)中:

...
recursive-include cython *
...

我不会将 mycythonmodule.c提交到版本控制“主干”(或 Mercurial 的“默认”)。当我发布一个版本时,我需要记住首先做一个 python setup.py build_ext,以确保对于源代码发布来说,mycythonmodule.c是存在的并且是最新的。我还创建了一个发布分支,并将 C 文件提交到该分支中。这样我就有了 C 文件的历史记录,这个文件是随着这个版本一起发布的。

添加到 Craig McQueen 的答案: 看下面如何覆盖 sdist命令,让 Cython 在创建源代码发行版之前自动编译源代码文件。

这样,您的运行没有意外发布过期 C源的风险。在你对分发过程的控制有限的情况下,例如当你从持续集成中自动创建分发过程时,它也是有帮助的。

from distutils.command.sdist import sdist as _sdist


...


class sdist(_sdist):
def run(self):
# Make sure the compiled Cython files in the distribution are up-to-date
from Cython.Build import cythonize
cythonize(['cython/mycythonmodule.pyx'])
_sdist.run(self)
cmdclass['sdist'] = sdist

Http://docs.cython.org/en/latest/src/userguide/source_files_and_compilation.html#distributing-cython-modules

强烈建议您分发生成的。C 文件以及 Cython 源文件,这样用户就可以安装模块,而无需使用 Cython。

还建议在分发的版本中默认情况下不启用 Cython 编译。即使用户已经安装了 Cython,他可能也不想仅仅为了安装模块而使用它。此外,他所拥有的版本可能与您使用的版本不同,并且可能无法正确编译您的源代码。

这只是意味着随附的 setup.py 文件将只是生成的。对于基本的例子,我们可以用:

from distutils.core import setup
from distutils.extension import Extension
 

setup(
ext_modules = [Extension("example", ["example.c"])]
)

这是我编写的一个安装脚本,它使得在构建中包含嵌套目录变得更加容易。需要从包中的文件夹运行它。

给出这样的结构:

__init__.py
setup.py
test.py
subdir/
__init__.py
anothertest.py

Setup.py

from setuptools import setup, Extension
from Cython.Distutils import build_ext
# from os import path
ext_names = (
'test',
'subdir.anothertest',
)


cmdclass = {'build_ext': build_ext}
# for modules in main dir
ext_modules = [
Extension(
ext,
[ext + ".py"],
)
for ext in ext_names if ext.find('.') < 0]
# for modules in subdir ONLY ONE LEVEL DOWN!!
# modify it if you need more !!!
ext_modules += [
Extension(
ext,
["/".join(ext.split('.')) + ".py"],
)
for ext in ext_names if ext.find('.') > 0]


setup(
name='name',
ext_modules=ext_modules,
cmdclass=cmdclass,
packages=["base", "base.subdir"],
)
#  Build --------------------------
#  python setup.py build_ext --inplace

编译愉快;)

包括(Cython)生成。C 文件很奇怪。特别是当我们把它加入 Git 的时候。我更喜欢用 Setuptools _ cython。当 Cython 不可用时,它将构建一个具有内置 Cython 环境的 egg,然后使用 egg 构建您的代码。

一个可能的例子: https://github.com/douban/greenify/blob/master/setup.py


更新(2017-01-05) :

setuptools 18.0开始,就不需要使用 setuptools_cython.给你就是一个不使用 setuptools_cython从头开始构建 Cython 项目的例子。

我想到的简单黑客技术:

from distutils.core import setup


try:
from Cython.Build import cythonize
except ImportError:
from pip import pip


pip.main(['install', 'cython'])


from Cython.Build import cythonize




setup(…)

如果无法导入 Cython,只需安装它。一个人可能不应该共享这段代码,但是对于我自己的依赖关系来说,它已经足够好了。

我发现只使用 setuptools 而不使用功能有限的 distutils 的最简单方法是

from setuptools import setup
from setuptools.extension import Extension
try:
from Cython.Build import cythonize
except ImportError:
use_cython = False
else:
use_cython = True


ext_modules = []
if use_cython:
ext_modules += cythonize('package/cython_module.pyx')
else:
ext_modules += [Extension('package.cython_module',
['package/cython_modules.c'])]


setup(name='package_name', ext_modules=ext_modules)

所有其他的答案要么依赖于

  • 蒸馏酒
  • Cython.Build导入,这在通过 setup_requires要求 cython 和导入 cython 之间产生了一个先有鸡还是先有蛋的问题。

一个现代的解决方案是使用 setuptools,参见 这个答案(自动处理 Cython 扩展需要 setuptools 18.0,也就是说,它已经可用很多年了)。具有需求处理、入口点和 cython 模块的现代标准 setup.py可能如下所示:

from setuptools import setup, Extension


with open('requirements.txt') as f:
requirements = f.read().splitlines()


setup(
name='MyPackage',
install_requires=requirements,
setup_requires=[
'setuptools>=18.0',  # automatically handles Cython extensions
'cython>=0.28.4',
],
entry_points={
'console_scripts': [
'mymain = mypackage.main:main',
],
},
ext_modules=[
Extension(
'mypackage.my_cython_module',
sources=['mypackage/my_cython_module.pyx'],
),
],
)

我认为我找到了一个非常好的方法,通过提供一个定制的 build_ext命令来实现这一点。这个想法是这样的:

  1. 我通过覆盖 finalize_options()并在函数体中执行 import numpy来添加 numpy 头,这很好地避免了在 setup()安装之前 numpy 不可用的问题。

  2. 如果 cython 在系统上可用,它会连接到命令的 check_extensions_list()方法,并通过 cythonize 所有过时的 cython 模块,将它们替换为 C 扩展,这些扩展稍后可以由 build_extension()方法处理。我们只是在我们的模块中提供了功能的后半部分: 这意味着如果 cython 不可用,但是我们提供了 C 扩展,它仍然可以工作,这允许您进行源代码分发。

密码是这样的:

import re, sys, os.path
from distutils import dep_util, log
from setuptools.command.build_ext import build_ext


try:
import Cython.Build
HAVE_CYTHON = True
except ImportError:
HAVE_CYTHON = False


class BuildExtWithNumpy(build_ext):
def check_cython(self, ext):
c_sources = []
for fname in ext.sources:
cname, matches = re.subn(r"(?i)\.pyx$", ".c", fname, 1)
c_sources.append(cname)
if matches and dep_util.newer(fname, cname):
if HAVE_CYTHON:
return ext
raise RuntimeError("Cython and C module unavailable")
ext.sources = c_sources
return ext


def check_extensions_list(self, extensions):
extensions = [self.check_cython(ext) for ext in extensions]
return build_ext.check_extensions_list(self, extensions)


def finalize_options(self):
import numpy as np
build_ext.finalize_options(self)
self.include_dirs.append(np.get_include())

这允许只编写 setup()参数,而不用担心导入和是否有 cython 可用:

setup(
# ...
ext_modules=[Extension("_my_fast_thing", ["src/_my_fast_thing.pyx"])],
setup_requires=['numpy'],
cmdclass={'build_ext': BuildExtWithNumpy}
)