cython代码编译和setup.py文件编写

Cython

官方文档: https://cython.readthedocs.io/en/latest/

中文文档:https://www.bookstack.cn/read/cython-doc-zh/
                   https://cython.apachecn.org/#/

构建 Cython 代码

与 Python 不同,Cython 代码必须编译。这发生在两个阶段:

.pyx文件由 Cython 编译为.c文件,包含 Python 扩展模块的代码。

.c文件由 C 编译器编译为.so文件(或 Windows 上的.pyd),可直接import直接进入 Python 会话.

构建 Cython 代码的几种方法:

  • 写一个 distutils / setuptools setup.py。推荐的方式。
  • 使用 Pyximport,导入 Cython .pyx文件就像它们是.py文件一样(使用 distutils 在后台编译和构建)。这种方法比编写setup.py更容易,但不是很灵活。因此,如果您需要某些编译选项,则需要编写setup.py
  • 手动运行cython命令行实用程序,从.pyx文件生成.c文件,然后手动将.c文件编译成适合从 Python 导入的共享库或 DLL。(这些手动步骤主要用于调试和实验。)
  • 使用 [Jupyter] 笔记本或 [Sage] 笔记本,两者都允许 Cython 代码内联。这是开始编写 Cython 代码并运行它的最简单方法。

 

手动从命令行编译

有两种从命令行编译的方法:

cython命令获取.py.pyx文件并将其编译为 C / C ++文件。

cythonize命令获取.py.pyx文件并将其编译为 C / C ++文件。然后,它将 C / C ++文件编译为可直接从 Python 导入的扩展模块。

cython命令进行编译

一种方法是使用 Cython 编译器手动编译它,例如:

$ cython primes.pyx

生成一个名为primes.c的文件,然后需要使用适合您平台的任何选项使用 C 编译器编译该文件以生成扩展模块(例如gcc)。

cythonize命令进行编译

cythonize -a -i yourmod.pyx

这将创建一个yourmod.c文件(或 C ++模式下的yourmod.cpp),对其进行编译,并将生成的扩展模块(.so.pyd,具体取决于您的平台)放在源文件旁边以进行直接导入(-i builds “in place””)。 -a 另外生成源代码的带注释的 html 文件。cythonize命令接受多个源文件和类似**/*.pyx的 glob 模式作为参数,并行构建作业的常用-j选项。在没有其他选项的情况下调用时,它只会将源文件转换为.c.cpp文件。传递-h标志以获取支持选项的完整列表。

distutils 构建 Cython 模块

def say_hello_to(name):
    print("Hello %s!" % name)

相应的setup.py脚本:

from distutils.core import setup
from Cython.Build import cythonize
setup(name='Hello world app',
      ext_modules=cythonize("hello.pyx"))

运行python setup.py build_ext --inplace。然后只需启动一个 Python 会话并执行from hello import say_hello_to并根据需要使用导入的函数。

如果您使用 setuptools 而不是 distutils,则需要注意,运行python setup.py install时的默认操作是创建一个压缩的egg文件,当您尝试从依赖包中使用它们时,这些文件无法与pxd文件一起用于pxd文件。为防止这种情况,请在setup()的参数中包含zip_safe=False

如果需要指定编译器选项,要链接的库或其他链接器选项,则需要手动创建Extension实例(请注意,glob 语法仍可用于在一行中指定多个扩展名): 

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
extensions = [
    Extension("primes", ["primes.pyx"],
        include_dirs=[...],
        libraries=[...],
        library_dirs=[...]),
    # Everything but primes.pyx is included here.
    Extension("*", ["*.pyx"],
        include_dirs=[...],
        libraries=[...],
        library_dirs=[...]),
]
setup(
    name="My hello app",
    ext_modules=cythonize(extensions),

使用 setuptools 时,您应该在 Cython 之前导入它,因为 setuptools 可能会替换 distutils 中的Extension

Cythonize参数 https://www.bookstack.cn/read/cython-doc-zh/docs-29.md

Cython.Build.cythonize(module_listexclude=Nonenthreads=0aliases=Nonequiet=Falseforce=Falselanguage=Noneexclude_failures=Falseshow_all_warnings=False**options)

  • module_list –作为模块列表,传递全局模式,全局模式列表或扩展对象列表。后者允许您通过常规distutils选项分别配置扩展。您还可以传递具有glob模式作为其源的Extension对象。然后,cythonize将解析该模式并为每个匹配文件创建扩展的副本。
  • exclude –当将glob模式传递作为module_list时,可以通过将某些模块名称传递到exclude选项中来显式排除某些模块名称。
  • nthreads –并行编译的并发构建数(需要multiprocessing模块)。
  • alias
  • quiet –如果为True,则Cython在编译过程中不会打印错误,警告或状态消息。
  • force –强制重新编译Cython模块,即使时间戳不表明需要重新编译也是如此。
  • language –要全局启用C ++模式,可以通过language='c++'。否则,这将基于编译器指令在每个文件级别确定。这仅影响基于文件名找到的模块。传入的扩展实例cythonize()将不会更改。建议使用编译器指令而不是此选项。# distutils: language = c++
  • exclude_failures –对于广泛的“尝试编译”模式,该模式将忽略编译失败并仅排除失败的扩展,请通过exclude_failures=True。请注意,这仅对编译.py文件有意义,这些文件也可以不经编译而使用。
  • show_all_warnings –默认情况下,并非所有Cython警告都会被打印。设置为true以显示所有警告。
  • annotate 
  • compiler_directives -允许集合中的编译器指令setup.py是这样的:compiler_directives={'embedsignature': True} 。请参阅编译器指令https://cython.readthedocs.io/en/latest/src/userguide/source_files_and_compilation.html#compiler-directives

包中的多个 Cython 文件

要自动编译多个 Cython 文件而不显式列出所有这些文件,可以使用 glob 模式:

setup(
    ext_modules = cythonize("package/*.pyx")
)

cythonize()传递它们,也可以在Extension对象中使用 glob 模式:

extensions = [Extension("*", ["*.pyx"])]
setup(
    ext_modules = cythonize(extensions)
)

如果您有许多扩展并希望避免声明中的额外复杂性,您可以使用它们的正常 Cython 源声明它们,然后在不使用 Cython 时调用以下函数而不是cythonize()来调整 Extensions 中的源列表:

import os.path
def no_cythonize(extensions, **_ignore):
    for extension in extensions:
        sources = []
        for sfile in extension.sources:
            path, ext = os.path.splitext(sfile)
            if ext in ('.pyx', '.py'):
                if extension.language == 'c++':
                    ext = '.cpp'
                else:
                    ext = '.c'
                sfile = path + ext
            sources.append(sfile)
        extension.sources[:] = sources
    return extensions

另一个选择是使 Cython 成为系统的设置依赖项,并使用 Cython 的 build_ext 模块作为构建过程的一部分运行

setup(
    setup_requires=[
        'cython>=0.x',
    ],
    extensions = [Extension("*", ["*.pyx"])],
    cmdclass={'build_ext': Cython.Build.build_ext},
    ...
)

通过cmdclass,build_ext可以自定义,看一个pytorch版本中的fasterrcnn 源码的setup.py

# --------------------------------------------------------
# Fast R-CNN
# Copyright (c) 2015 Microsoft
# Licensed under The MIT License [see LICENSE for details]
# Written by Ross Girshick
# --------------------------------------------------------

import os
from os.path import join as pjoin
import numpy as np
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
from Cython.Build import cythonize

def find_in_path(name, path):
    "Find a file in a search path"
    # adapted fom http://code.activestate.com/recipes/52224-find-a-file-given-a-search-path/
    for dir in path.split(os.pathsep):
        binpath = pjoin(dir, name)
        if os.path.exists(binpath):
            return os.path.abspath(binpath)
    return None


def locate_cuda():
    """Locate the CUDA environment on the system

    Returns a dict with keys 'home', 'nvcc', 'include', and 'lib64'
    and values giving the absolute path to each directory.

    Starts by looking for the CUDAHOME env variable. If not found, everything
    is based on finding 'nvcc' in the PATH.
    """

    # first check if the CUDAHOME env variable is in use
    if 'CUDAHOME' in os.environ:
        home = os.environ['CUDAHOME']
        nvcc = pjoin(home, 'bin', 'nvcc')
    else:
        # otherwise, search the PATH for NVCC
        default_path = pjoin(os.sep, 'usr', 'local', 'cuda', 'bin')
        nvcc = find_in_path('nvcc', os.environ['PATH'] + os.pathsep + default_path)
        if nvcc is None:
            raise EnvironmentError('The nvcc binary could not be '
                                   'located in your $PATH. Either add it to your path, or set $CUDAHOME')
        home = os.path.dirname(os.path.dirname(nvcc))

    cudaconfig = {'home': home, 'nvcc': nvcc,
                  'include': pjoin(home, 'include'),
                  'lib64': pjoin(home, 'lib64')}
    for k, v in cudaconfig.iteritems():
        if not os.path.exists(v):
            raise EnvironmentError('The CUDA %s path could not be located in %s' % (k, v))

    return cudaconfig


CUDA = locate_cuda()

# Obtain the numpy include directory.  This logic works across numpy versions.
try:
    numpy_include = np.get_include()
except AttributeError:
    numpy_include = np.get_numpy_include()


def customize_compiler_for_nvcc(self):
    """inject deep into distutils to customize how the dispatch
    to gcc/nvcc works.

    If you subclass UnixCCompiler, it's not trivial to get your subclass
    injected in, and still have the right customizations (i.e.
    distutils.sysconfig.customize_compiler) run on it. So instead of going
    the OO route, I have this. Note, it's kindof like a wierd functional
    subclassing going on."""

    # tell the compiler it can processes .cu
    self.src_extensions.append('.cu')

    # save references to the default compiler_so and _comple methods
    default_compiler_so = self.compiler_so
    super = self._compile

    # now redefine the _compile method. This gets executed for each
    # object but distutils doesn't have the ability to change compilers
    # based on source extension: we add it.
    def _compile(obj, src, ext, cc_args, extra_postargs, pp_opts):
        print extra_postargs
        if os.path.splitext(src)[1] == '.cu':
            # use the cuda for .cu files
            self.set_executable('compiler_so', CUDA['nvcc'])
            # use only a subset of the extra_postargs, which are 1-1 translated
            # from the extra_compile_args in the Extension class
            postargs = extra_postargs['nvcc']
        else:
            postargs = extra_postargs['gcc']

        super(obj, src, ext, cc_args, postargs, pp_opts)
        # reset the default compiler_so, which we might have changed for cuda
        self.compiler_so = default_compiler_so

    # inject our redefined _compile method into the class
    self._compile = _compile


# run the customize_compiler
class custom_build_ext(build_ext):
    def build_extensions(self):
        customize_compiler_for_nvcc(self.compiler)
        build_ext.build_extensions(self)


ext_modules = [
    Extension(		
        "model.utils.cython_bbox",		
        ["model/utils/bbox.pyx"],		
        extra_compile_args={'gcc': ["-Wno-cpp", "-Wno-unused-function"]},		
        include_dirs=[numpy_include]		
    ),
    Extension(
        'pycocotools._mask',
        sources=['pycocotools/maskApi.c', 'pycocotools/_mask.pyx'],
        include_dirs = [np.get_include(), '../common'],
        extra_compile_args={'gcc': ['-Wno-cpp', '-Wno-unused-function', '-std=c99']},
    ),
]

setup(
    name='faster_rcnn',
    ext_modules=ext_modules,
    # inject our custom trigger
    cmdclass={'build_ext': custom_build_ext},
)

setup(name='pycocotools',
      packages=['pycocotools'],
      package_dir = {'pycocotools': 'pycocotools'},
      version='2.0',
      ext_modules=
          cythonize(ext_modules)
      )

Cython + distutils/setuptools 构建

关于编写setup来变异cython程序,有两个模块distutils和setuptools,distutils属于python的标准库,setuptools在distutils基础上补充扩展,并且更新维护也比较快,所以setuptools应该替代distutils.在看很多项目源码里还是使用distutils,主要还是因为它是标准库吧,setuptools是三方库还要安装.distutils的官方文档也标注是遗留版本.(https://docs.python.org/zh-cn/3/distutils/index.html#distutils-index)

编写setup文件

,这里不记录怎么分发安装,简单了解下setup文件,用来编译cython

分发一个foo文件中包含的名为的模块foo.py

from distutils.core import setup
setup(name='foo',
      version='1.0',
      py_modules=['foo'],
      )
py_modules 需要的模块列表,模块是通过模块名称而不是文件名指定的,其他的是元数据(名称,版本号)等.

setup的参数

参数名称

类型

name

包的名字

字符串

version

包的版本号;参见 distutils.version

字符串

dcription

单行的包的描述

字符串

long_description

更长的包描述

字符串

author

包的作者

字符串

author_email

包的作者的电子邮件

字符串

maintainer

当前维护者的名称(如果不同于作者)。请注意,如果提供了维护程序,则distutils会将其用作作者PKG-INFO

字符串

maintenanceer_email

当前维护者的电子邮件地址,如果不同与作者

字符串

url

包的URL(主页)

字符串

download_url

包的下载地址

字符串

packages

distutils将操作的Python软件包列表

字符串列表

py_modules

distutils 会操作的 Python 模块列表

字符串列表

scripts

要构建和安装的独立脚本文件的列表

字符串列表

ext_modules

要构建的 Python 扩展的列表

类 distutils.core.Extension 的实例的列表

classifiers

包的类别列表

字符串列表;有效的分类器列在PyPI上

distclass

要使用的类 Distribution 类

distutils.core.Distribution 的子类

script_name

setup.py 脚本名称 —— 默认为 sys.argv[0]

字符串

script_args

提供给安装脚本的参数

字符串列表

options

安装脚本的默认选项

字典

license

包的许可证

字符串

keywords

描述性元数据,请参阅 PEP 314

字符串列表或逗号分隔的字符串

platform

 

字符串列表或逗号分隔的字符串

cmdclass

命令名称到Command子类的映射

字典

datafile

要安装的数据文件列表

列表

package_dir

包到目录名称的映射

字典

#!/usr/bin/env python

from distutils.core import setup

setup(name='Distutils',
      version='1.0',
      description='Python Distribution Utilities',
      author='Greg Ward',
      author_email='gward@python.net',
      url='https://www.python.org/sigs/distutils-sig/',
      packages=['distutils', 'distutils.command'],
     )

packages  按包分发,其他的还是元数据,其中name最好是一定要到,如果安装的话生成的文件会是指定name的名字.          Setup函数的packages参数是一个列表,其中包含了Distutils需要处理(构建、发布、安装等)的所有包。要实现此目的,那么包名和目录名必须能够相互对应,比如包名是distutils,则意味着在发布的根目录(setup脚本所在目录)下存在distutils子目录;再比如在setup脚本中packages = ['foo'],意味着要在setup脚本所在目录下存在相应的foo目录和foo/__init__.py文件。
package_dir选项可以改变这种默认的对应规则。package_dir是个字典,其中的key是要安装的包名,如果为空,则表明是root package,value就是该包(key)对应的源码树的目录。

package_dir = {'':'lib'},
packages = ['foo']

则必须在目录中存在lib子目录,lib/foo子目录,以及文件lib/foo/__init__.py。所以源码树如下

setup.py
lib/
    foo/
        __init__.py
        foo.py

 另外一个例子,foo包对应lib目录,所以,foo.bar包就对应着lib/bar子目录。所以如果在setup.py中这么写:

package_dir = {'foo':'lib'},
      packages = ['foo','foo.bar']

则必须存在lib/__init__.py,  lib/bar/__init__.py文件。源码树如下: 

setup.py
lib/
    __init__.py
    foo.py
    bar/
        __init__.py
        bar.py

Python扩展模块必须指定扩展名,源文件以及所有编译/链接要求(包括目录,要链接的库等),通过ext_modules

如果不需要对编译器/链接器的附加说明,则描述此扩展非常简单:

Extension('foo', ['foo.c'])
from distutils.core import setup, Extension
setup(name='foo',
      version='1.0',
      ext_modules=[Extension('foo', ['foo.c'])],
      )
Extension('foo', ['src/foo1.c', 'src/foo2.c'])和 Extension('pkg.foo', ['src/foo1.c', 'src/foo2.c'])区别是生成的扩展名在文件系统中的位置
如果一个包下有多个扩展,而且要把这些扩展都放在统一的目录下,则可以使用ext_package关键字,比如下面的语句
setup(...,
      ext_package='pkg',
      ext_modules=[Extension('foo', ['src/foo.c']),
                   Extension('subpkg.bar', ['src/bar.c'])]
     )

如果扩展名include 在分发根目录下的目录中需要头文件,可以使用以下include_dirs选项

Extension('foo', ['foo.c'], include_dirs=['include'])

以使用define_macros和 undef_macros选项定义和取消定义预处理器宏 

Extension(...,
    define_macros=[('NDEBUG', '1'),
                    ('HAVE_STRFTIME', None)],
    undef_macros=['HAVE_FOO', 'HAVE_BAR'])

等于在每个C源文件的顶部都包含此代码:

#define NDEBUG 1
#define HAVE_STRFTIME
#undef HAVE_FOO
#undef HAVE_BAR

指定在构建扩展时要链接到的库,以及用于搜索这些库的目录。该libraries选项是要链接的库列表,library_dirs是在链接时搜索库runtime_library_dirs的目录列表,并且是在运行时搜索共享(动态加载)库的目录列表。

Extension(...,
          libraries=['gdbm', 'readline'])

Extension参数

参数名称

类型

name

扩展名的全名,包括所有软件包- 不是文件名或路径名,而是Python点分名称

字符串

source

相对于分发根(安装脚本所在的位置)的源文件名列表,以Unix形式(以斜杠分隔)以实现可移植性。源文件可以是C,C ++,SWIG(.i),特定于平台的资源文件,也可以是build_ext命令识别为Python扩展源的任何其他文件 。

字符串列表

include_dirs

搜索C / C ++头文件的目录列表(以Unix形式提供可移植性)

字符串列表

define_macros

要定义的宏列表;每个宏都是使用2元组定义的,其中value是用来定义它的字符串,或者是在没有特定值的情况下定义它的字符串(等效 于source或 在Unix C编译器命令行上)(name, value)None#define FOO-DFOO

元组列表

undef_macros

要明确取消定义的宏列表

字符串列表

library_dirs

在链接时搜索C / C ++库的目录列表

字符串列表

libraries

要链接的库名列表(不是文件名或路径)

字符串列表

runtime_library_dirs

在运行时搜索C / C ++库的目录列表(对于共享扩展,这是在加载扩展时)

字符串列表

extra_objects

要链接的其他文件列表(例如,“源”未暗示的目标文件,必须明确指定的静态库,二进制资源文件等)

字符串列表

extra_compile_args

在“源”中编译源文件时要使用的任何其他特定于平台和编译器的信息。对于需要命令行的平台和编译器,这通常是命令行参数的列表,但是对于其他平台,可以是任何东西。

字符串列表

extra_link_args

将目标文件链接在一起以创建扩展名(或创建新的静态Python解释器)时使用的所有其他特定于平台和编译器的信息。与“ extra_compile_args”类似的解释。

字符串列表

export_symbols

从共享扩展名导出的符号列表。并非在所有平台上都使用,对于Python扩展通常也不是必需的,Python扩展通常只导出一个符号: init+ extension_name。

字符串列表

depends

扩展名依赖的文件列表

字符串列表

language

扩展语言(即 'c''c++', 'objc')。如果未提供,将从源扩展中检测到。

字符串

optional

指定扩展中的构建失败不应中止构建过程,而只是跳过扩展。

布尔

 

Distutils简单示例 https://docs.python.org/zh-cn/3/distutils/examples.html

扩展 Distutils https://docs.python.org/zh-cn/3/distutils/extending.html

Distutils 可以通过各种方式扩展。 大多数扩展都采用新命令或现有命令的替换形式。 例如,可以编写新命令以支持新的特定于平台的包格式,但是可以修改现有命令的替换,以修改命令在包上的操作细节。

distutils 的大多数扩展都在想要修改现有命令的 setup.py 脚本中编写;其中许多只是简单地在 .py 文件以外添加了一些应当被拷贝到包中的文件后缀以便使用。

大多部 distutils 命令的实现都是 distutils.cmd.Command 类的子类。 新增命令可直接继承自 Command,而替换命令往往间接派生自 Command, 直接子类化它们所替换的命令。 所有命令都要求自 Command 派生。

from distutils.command.build_py import build_py as _build_py
from distutils.core import setup

class build_py(_build_py):
    """Specialized Python source builder."""

    # implement whatever needs to be different...

setup(cmdclass={'build_py': build_py},
      ...)

Setuptools文档 https://setuptools.readthedocs.io/en/latest/index.html

 

 

 

 

 

  • 2
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值