【问题标题】:How should I structure a Python package that contains Cython code我应该如何构建包含 Cython 代码的 Python 包
【发布时间】:2011-05-29 04:35:18
【问题描述】:

我想制作一个 Python 包,其中包含一些 Cython 代码。我的 Cython 代码运行良好。但是,现在我想知道如何最好地打包它。

对于大多数只想安装软件包的人,我想包含 Cython 创建的 .c 文件,并安排 setup.py 编译该文件以生成模块。那么用户不需要安装 Cython 来安装包。

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

我应该如何构建包中的文件以适应这两种情况?

Cython documentation gives a little guidance。但它并没有说明如何制作单个 setup.py 来处理有/没有 Cython 的情况。

【问题讨论】:

  • 我看到这个问题得到的赞成票比任何答案都多。我很想知道为什么人们会觉得答案不令人满意。
  • 我找到了this section of the documentation,它给出了准确的答案。

标签: python packaging cython


【解决方案1】:

我现在自己在 Python 包中完成了这项工作 simplerandom (BitBucket repo - 编辑:现在是 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 文件的历史记录。

【讨论】:

  • 谢谢,这正是我正在开放的 Pyrex 项目所需要的! MANIFEST.in 让我一时不爽,但我只需要那一行。出于兴趣,我将 C 文件包含在源代码管理中,但我认为您的观点是不必要的。
  • 我已经编辑了我的答案,以解释 C 文件如何不在主干/默认中,而是添加到发布分支中。
  • @CraigMcQueen 感谢您的出色回答,它对我帮助很大!但是,我想知道,在可用时使用 Cython 是理想的行为吗?在我看来,默认情况下使用预先生成的 c 文件会更好,除非用户明确想要使用 Cython,在这种情况下他可以设置环境变量或其他东西。这将使安装更加稳定/健壮,因为用户可能会根据他安装的 Cython 版本获得不同的结果 - 他甚至可能不知道他已经安装了它并且它正在影响包的构建。
【解决方案2】:

添加到 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

【讨论】:

    【解决方案3】:

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

    强烈建议您分发生成的 .c 文件以及 Cython 源代码,以便用户无需 Cython 即可安装您的模块。

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

    这只是意味着您附带的 setup.py 文件将只是生成的 .c 文件上的普通 distutils 文件,对于我们将拥有的基本示例:

    from distutils.core import setup
    from distutils.extension import Extension
     
    setup(
        ext_modules = [Extension("example", ["example.c"])]
    )
    

    【讨论】:

      【解决方案4】:

      最简单的方法是包含两者但只使用 c 文件?包含 .pyx 文件很好,但是一旦有了 .c 文件就不需要了。想要重新编译 .pyx 的人可以安装 Pyrex 并手动进行。

      否则,您需要为 distutils 自定义 build_ext 命令,该命令首先构建 C 文件。 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
      

      应该处理它。

      【讨论】:

      • 感谢您的回答。这是合理的,尽管我更喜欢在安装 Cython 时 setup.py 可以直接从 .pyx 文件构建。 My answer 也实现了这一点。
      • 嗯,这就是我回答的重点。这不是一个完整的 setup.py。
      【解决方案5】:

      包含 (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_cythonHere 是一个在没有 setuptools_cython 的情况下从头开始构建 Cython 项目的示例。

      【讨论】:

      • 这是否解决了即使您在 setup_requires 中指定 Cython 也无法安装的问题?
      • 也不能将'setuptools>=18.0' 放入setup_requires 而不是创建方法is_installed
      • @capa但是如果您使用的是 setuptools setuptools_cython。
      • 感谢@McKelvin,这似乎是一个很好的解决方案!有什么理由为什么我们应该使用另一种方法,在此旁边预先对源文件进行cythonizing?我试过你的方法,安装时似乎有点慢(安装需要一分钟,但构建需要一秒钟)。
      • @Martinsos pip install wheel。那么一定是原因1。请先安装轮子再试一次。
      【解决方案6】:

      我想出的简单技巧:

      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。可能不应该共享此代码,但对于我自己的依赖项来说已经足够了。

      【讨论】:

        【解决方案7】:

        所有其他答案要么依赖于

        • distutils
        • Cython.Build 导入,这会在通过 setup_requires 要求 cython 和导入它之间产生先有鸡还是先有蛋的问题。

        现代解决方案是改用 setuptools,请参阅 this answer(自动处理 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'],
                ),
            ],
        )
        

        【讨论】:

        • 在设置时从 Cython.Build 导入会导致 ImportError。使用 setuptools 编译 pyx 是最好的方法。
        【解决方案8】:

        这是我编写的设置脚本,它可以更轻松地在构建中包含嵌套目录。需要从包中的文件夹运行它。

        Givig 结构如下:

        __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
        

        编译愉快 ;)

        【讨论】:

          【解决方案9】:

          我发现只使用 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)
          

          【讨论】:

          • 事实上,使用 setuptools 不需要从 Cython.Build 显式导入 try/catched,请参阅我的答案。
          【解决方案10】:

          我想我通过提供自定义build_ext 命令找到了一个很好的方法。思路如下:

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

            1234563 .我们也只是在我们的模块中提供了功能的后半部分:这意味着如果 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}
              )
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-07-17
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多