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_list, exclude=None, nthreads=0, aliases=None, quiet=False, force=False, language=None, exclude_failures=False, show_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 | 包的版本号;参见 | 字符串 |
dcription | 单行的包的描述 | 字符串 |
long_description | 更长的包描述 | 字符串 |
author | 包的作者 | 字符串 |
author_email | 包的作者的电子邮件 | 字符串 |
maintainer | 当前维护者的名称(如果不同于作者)。请注意,如果提供了维护程序,则distutils会将其用作作者 | 字符串 |
maintenanceer_email | 当前维护者的电子邮件地址,如果不同与作者 | 字符串 |
url | 包的URL(主页) | 字符串 |
download_url | 包的下载地址 | 字符串 |
packages | distutils将操作的Python软件包列表 | 字符串列表 |
py_modules | distutils 会操作的 Python 模块列表 | 字符串列表 |
scripts | 要构建和安装的独立脚本文件的列表 | 字符串列表 |
ext_modules | 要构建的 Python 扩展的列表 | 类 |
classifiers | 包的类别列表 | 字符串列表;有效的分类器列在PyPI上。 |
distclass | 要使用的类 | |
script_name | setup.py 脚本名称 —— 默认为 | 字符串 |
script_args | 提供给安装脚本的参数 | 字符串列表 |
options | 安装脚本的默认选项 | 字典 |
license | 包的许可证 | 字符串 |
keywords | 描述性元数据,请参阅 PEP 314 | 字符串列表或逗号分隔的字符串 |
platform | 字符串列表或逗号分隔的字符串 | |
cmdclass | 命令名称到 | 字典 |
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编译器命令行上) | 元组列表 |
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扩展通常只导出一个符号: | 字符串列表 |
depends | 扩展名依赖的文件列表 | 字符串列表 |
language | 扩展语言(即 | 字符串 |
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