NumPy 基础知识(二)
七、构建和分发 NumPy 代码
在现实世界中,您将编写一个应用,以将其分发到世界上或在其他各种计算机上重用。 为此,您希望应用以标准方式打包,以便社区中的每个人都能理解和遵循。 正如您现在已经注意到的那样,Python 用户主要使用名为pip的包管理器来自动安装其他程序员创建的模块。 Python 具有一个称为 PyPI(Python 包索引)的打包平台,该平台是 50,000 多个 Python 包的官方中央存储库。 一旦在 PyPi(又名 Cheese Shop)中注册了包,世界各地的其他用户都可以在使用pip等包管理系统对其进行配置后进行安装。 Python 随附了许多解决方案,可帮助您构建代码以准备分发给 Cheese Shop ,并且在本章中,我们将重点介绍两个此类工具,setuptools 和Distutils 除了这两个工具之外,我们还将研究 NumPy 提供的称为numpy.distutils的特定模块。 该模块使程序员更容易构建和分发特定于 NumPy 的代码。 该模块还提供了其他函数,例如用于编译 Fortran 代码,调用f2py等方法。 在本章中,我们将通过以下步骤来学习包装工作流程:
- 我们将建立一个小的但可行的设置
- 我们将说明将 NumPy 模块集成到您的设置中的步骤
- 我们将说明如何在互联网上注册和分发您的应用
Distutils 和 Setuptools 简介
在开始之前,首先让我们了解这些工具是什么以及为什么我们偏爱另一个工具。 Distutils是 Python 默认提供的框架,setuptools建立在标准Distutils的基础上,以提供增强的功能和特性。 在现实世界中,您将永远不会使用Distutils。 您可能想单独使用Distutils的唯一情况是setuptools不可用。 (良好的设置脚本应在继续之前检查setuptools的可用性。)在大多数情况下,用户最好安装setuptools,因为当今大多数包都是基于它们构建的。 在接下来的章节中,我们将使用setuptools来构建 Cython 代码; 因此,出于我们的目的,我们现在将安装setuptools并从现在开始广泛使用它。
接下来,让我们从安装必需的工具开始,以构建我们的第一个虚拟(但有效)安装程序。 安装程序正常运行后,我们将在 Pandas 脚本模块的真实脚本中深入介绍 NumPy 的更多功能。 我们将研究脚本中进行的检查,以使其更强大,以及在发生故障时如何提供更多信息。
准备工具
要在您的系统上安装setuptools,您需要先从这里下载系统中的ez_setup.py,然后从命令提示符处执行以下操作 :
$ python ez_setup.py
要测试setuptools的安装,请打开 Python shell 并键入以下内容:
> import setuptools
如果前面的导入没有给出任何错误,则说明我们已成功安装setuptools。
建立第一个有效的发行版
我们前面提到的所有工具(setuptools,Distutils和numpy.distutils)都围绕setup函数。 为了了解大多数包装要求,我们将研究一个简单的setup函数,然后研究一个成熟的安装程序。 要创建基本的安装程序,我们需要使用有关包的元数据调用setup函数。 让我们叫第一个包py_hello,它只有一个函数greeter,并且在调用时只打印一条消息。 可从 Bitbucket 存储库下载该包。该项目的目录结构如下:
py_hello
├── README
├── MANIFEST.in
├── setup.py
├── bin
│ └── greeter.bat
└── greeter
├── __init__.py
├── greeter.py
让我们在这里看一些标准文件:
README- 此文件用于存储有关您的项目的信息。 该文件不是系统所需的文件,如果没有该文件,您仍将获得安装程序的构建,但是将其保留在此处是一种很好的做法。MANIFEST.in- 这是一个文本文件,Distutils使用该文本文件来收集项目中的所有文件。 这非常重要,只有此处列出的文件才会进入最终安装程序tar存档。 除了指定最终安装程序中应包含的文件之外,manifest还可以用于从项目目录中排除某些文件。manifest文件是必需的; 如果不存在,则在使用setup.py时会出现错误。 如果您具有svn设置,则可以使用sdist命令通过解析.svn文件并构建manifest.in文件来自动包含文件。__init__.py- 该文件对于 Python 将该目录识别为模块很重要。 创建后可以将其留空。
要为此安装程序创建安装程序,我们在根目录中有setup.py,它使用setuptools中的setup函数:
from setuptools import setup
import os
description = open(os.path.join(os.path.dirname(__file__), 'README'), 'r').read()
setup(
name = "py_hello",
packages = ["greeter"],
scripts = ["bin/greeter.bat"],
include_package_data = True,
package_data = {
"py_hello":[]
},
version = "0.1.0",
description = "Simple Application",
author = "packt",
author_email = "packt@packt.com",
url = "https://bitbucket.org/tdatta/book/py_hello",
download_url = "https://bitbucket.org/tdatta/book/py_hello/zipball/master",
keywords = ["tanmay", "example_seutp", "packt" "app"],
install_requires=[
"setup >= 0.1"],
license='LICENSE',
classifiers = [
"Programming Language :: Python",
"Development Status :: release 0.1",
"Intended Audience :: new users",
"License :: Public",
"Operating System :: POSIX :: Linux",
"Topic :: Demo",
],
long_description = description
)
以下是安装程序中使用的选项:
name- 这是安装 TAR 归档文件的名称。packages- 这是一个列出要包含的包的列表。scripts- 这是要安装到/usr/bin.等标准位置的脚本的列表。在此特定情况下,仅存在一个 echo 脚本。 这样做的目的是向读者展示如何将包附带脚本。package_data- 这是字典,具有与文件列表相关联的键(包)。version- 这是您的项目的版本。 这将附加到安装程序名称的末尾。long_description- 在 PyPI 网站上显示时,它将转换为 HTML。 它应该包含有关您的项目打算提供的信息。 您可以直接在脚本中编写它; 但是,最佳实践是维护README文件并从此处读取说明。install_required- 这是用于添加安装依赖项的列表。 您将添加代码中使用的第三方模块的名称和版本。 请注意遵循约定以在此处指定版本。classifiers- 当您在 PyPI 网站上上传包时,将选中此选项。 您应该从以下网站提供的选项中进行选择。
现在,使用build选项运行setup.py应该不会给您任何错误,并生成带有.egg-info后缀的文件夹。 此时,您可以使用sdist选项运行setup.py,并创建一个可以与世界共享的包。
您应该看到最终消息为创建 tar 归档文件,如下所示:
要测试该包,可以按照以下步骤将其安装在本地计算机上:
python setup.py install
并按以下所示检查它:
这时,在cmd/bash提示符下写greeter,您将看到一条消息does nothing。 该回显消息来自greeter.bat,我们将其放置在安装文件的scripts键中。
下一部分可以添加到此框架setup.py以包括NumPy特定的功能。
添加 NumPy 和非 Python 源代码
接下来,我们将研究一些特定于 NumPy 的代码,并了解如何提高设置的错误处理能力; 通常,我们将探索一些良好的编程习惯。 我们还将展示如何将非 Python 源(c,fortran或f2py)添加到安装程序中。 以下分析显示了完整代码的一部分,您可以在随附的代码文件中或在这个页面中找到这些代码:
if sys.version_info[0] < 3:
import __builtin__ as builtins
else:
import builtins
.....
.....
.....
For full sample look for setup.py file with the accompanying CD
.....
.....
##define a function to import numpy if available and return true else false
def is_numpy_installed():
try:
import numpy
except ImportError:
return False
return True
## next we will import setuptools feature here
## We need to do this here because setuptools will "Monkey patch" the setup function
##
SETUPTOOLS_COMMANDS = set([
'develop', 'release', 'bdist_egg','bdist_rpm',
'bdist_wininst', 'install_egg_info', 'build_sphinx',
'easy_install', 'upload', 'bdist_wheel',
'--single-version-externally-managed',
])
if SETUPTOOLS_COMMANDS.intersection(sys.argv):
import setuptools
extra_setuptools_args = dict(
zip_safe-False # Custom clean command to remove build artifacts
## The main function where we link everything
def setup_package():
# check NumPy and raise excpetions
if is_numpy_installed() is False:
raise ImportError("Numerical Python (NumPy) is not installed. The package requires it be installed. Installation instruction available at the NumPy website")
from numpy.distutils.core import setup, Extension
# add extension from Fortran
ext1 = Extension(name = "firstExt",
sources = ['firstExt.f'])
ext2 = Extension(name = "convolutedExt",
sources = ['convolutedExt.pyf, stc2.f'],
include_dir = ['paths to include'],
extra_objects = "staticlib.a")
metadata = dict(name = "yourPackage",
description="short desc",
license = "licence info here",
ext_modules = [ext1, ext2]
..
# metadata as we set previously
..
**extra_setuptools_args
)
setup(**metadata)
if __name__ == "__main__":
setup_package()
上面的脚本从完整的工作设置中删除,着重于几乎所有设置脚本中都可以找到的某些方面。 这些任务确保您已完成足够的错误处理,并且脚本在不解释/提示下一步操作的情况下不会失败:
- 检查是否已安装 NumPy。 此处用来确保已安装 NumPy 的模式是一种标准模式,您可以将其用于计划使用的所有模块,并且是安装程序所必需的。 为了执行此任务,我们首先构建一个函数
is_numpy_installed尝试导入numpy并返回一个布尔值。 您可能会为安装文件可能使用的所有外部包创建类似的函数。 高级用户可以使用 Python 装饰器来以更优雅的方式进行处理。 如果此函数返回错误值,则安装程序应输出警告/信息,以防没有此包无法完成安装。 - 将
Extensions添加到设置文件中。 Extension类是使我们能够向安装程序中添加非 Python 代码的对象。sources参数可能包含 Fortran 源文件列表。 但是,列表源可能最多包含一个f2py签名文件,然后扩展模块的名称必须与签名文件中使用的<module>匹配。f2py签名文件必须恰好包含一个 Python 模块块,否则安装程序将无法构建。 您可以决定不在sources参数中添加签名文件。 在这种情况下,f2py将扫描 Fortran 源文件以获取常规签名,以构造 Fortran 代码的包装器。 可以使用Extension类参数f2py_options来指定f2py进程的其他选项。 这些选项不在本书的讨论范围内,大多数读者不会使用它们。 有关更多详细信息,用户可以参考api文档中的numpy.distutils扩展类。
可以按以下方式测试安装文件:
$ python <setup.py file> build_src build_ext --help
这里的build_src参数用于构造 Fortran 包装器扩展模块。 这里假定用户在其计算机上安装了 C/C++ 和 Fortran 编译器。
测试您的包
非常重要的一点是,所构建的包可以在用户的计算机上正常运行/安装。 因此,您应该花时间测试包。 测试安装背后的总体思路是创建一个 VirtualEnv 并尝试安装该包或完全使用另一个系统。 在此阶段遇到的任何错误都应删除,并且作者应尝试确保更容易遵循这些异常。 异常也应尝试提供解决方案。 此阶段的常见错误是:
- 关于预装模块和库的假设。
- 开发人员可能会忘记在安装文件中包含依赖项。 如果使用新的 VirtualEnv 来测试安装程序,则会捕获此错误。
- 权限和提升权限的要求。
- 某些用户可能对计算机具有只读访问权限。 这很容易被忽略,因为大多数开发人员在自己的机器上都没有这种情况。 如果包的提供者遵循正确的方法来选择要写入的目录,则应该不会出现此问题。 通常,通过使用没有管理员访问权限的用户测试脚本来检查这种情况是一种很好的做法。
分发您的应用
完成模块/应用的所有开发并准备好完整的正常工作的应用和设置文件后,下一个任务就是与世界分享您的辛勤工作,使他人受益。 使用 PyPI 将其发布到全世界的步骤非常简单。 作为包作者,您需要做的第一件事就是注册自己。 您可以直接从命令行执行以下操作:
**$ python setup.py register
running register
running egg_info
....
....
We need to know who you are, so please choose either:
1\. use your existing login,
2\. register as a new user,
3\. have the server generate a new password for you (and email it to you), or
4\. quit
Your selection [default 1]:**
提示
如果setup.py中缺少任何文件的正确元数据信息,则此过程将失败。 确保首先工作setup.py。
最后,您可以通过执行以下操作在 PyPI 上上传您的发行版:
$ python setup.py sdist upload
希望,如果您正确键入了所有内容,您的应用将被打包并在 PyPI 上供世界使用。
总结
在本章中,我们介绍了用于打包和分发应用的工具。 我们首先看了一个更简单的setup.py文件。 您研究了setup函数的属性以及这些参数如何链接到最终安装程序。 接下来,我们添加了与 NumPy 相关的代码,并添加了一些异常处理代码。 最后,我们构建了安装程序并学习了如何在 Cheese Shop(PyPI 网站)上上传它。 在下一章中,您将研究通过将 Python 代码的一部分转换为 Cython 来进一步加速 Python 代码的方法。
八、使用 Cython 加速 NumPy
Python 与 NumPy 库相结合为用户提供了编写高度复杂的函数和分析的工具。 随着代码的大小和复杂性的增长,代码库中的低效率问题开始蔓延。一旦项目进入完成阶段,开发人员就应开始关注代码的性能并分析瓶颈。 Python 提供了许多工具和库来创建优化且性能更快的代码。
在本章中,我们将研究一种名为 Cython 的工具。 Cython 是 Python 和“Cython”语言的静态编译器,在从事科学库/数值计算的开发人员中特别流行。 许多用 Python 编写的著名分析库都大量使用 Cython(Pandas,SciPy,scikit-learn 等)。
Cython 编程语言是 Python 的超集,用户仍然喜欢 Python 所提供的所有功能和更高层次的结构。 在本章中,我们将研究 Cython 起作用的许多原因,并且您将学习如何将 Python 代码转换为 Cython。 但是,本章不是 Cython 的完整指南。
在本章中,我们将介绍以下主题:
- 在我们的计算机上安装 Cython
- 将少量 Python 代码重写为 Cython 版本并进行分析
- 学习在 Cython 中使用 NumPy
优化代码的第一步
每个开发人员在优化其代码时应注意的问题如下:
- 您的代码执行多少个函数调用?
- 有多余的调用吗?
- 该代码使用了多少内存?
- 是否存在内存泄漏?
- 瓶颈在哪里?
前四个问题主要由分析器工具回答。 建议您至少学习一种分析工具。 分析工具将不在本章中介绍。 在大多数情况下,建议先尝试优化函数调用和内存使用,然后再使用低级方法,例如 Cython 或汇编语言(使用 C 衍生语言)。
一旦确定了瓶颈,并且解决了算法和逻辑的所有问题,Python 开发人员便可以进入 Cython 的世界,以提高应用的速度。
设置 Cython
Cython 是一个将类型定义的 Python 代码转换为 C 代码的编译器,该代码仍在 Python 环境中运行。 最终输出是本机代码,其运行速度比 Python 生成的字节码快得多。 在大量使用循环的代码中,Python 代码加速的幅度更加明显。 为了编译 C 代码,首要条件是在计算机上安装 C/C++ 编译器,例如gcc(Linux)或mingw(Windows)。
第二步是安装 Cython。 Cython 与其他带有 Python 模块的库一样,可以使用任何首选的方法(PIP,EasyInstall 等)进行安装。 完成这两个步骤后,您可以通过尝试从 Shell 调用 Cython 来测试设置。 如果收到错误消息,则说明您错过了第二步,需要重新安装 Cython 或从 Cython 官方网站下载 TAR 归档文件,然后从这次下载的root文件夹中运行以下命令:
python setup.py install
正确完成所有操作后,您可以继续使用 Cython 编写第一个程序。
Cython 中的 Helloworld
Cython 程序看起来与 Python 程序非常相似,但大多带有附加的类型信息。 让我们看一个简单的程序,该程序计算给定n的第n个斐波那契数:
defcompute_fibonacchi(n):
"""
Computes fibonacchi sequence
"""
a = 1
b = 1
intermediate = 0
for x in xrange(n):
intermediate = a
a = a + b
b = intermediate
return a
让我们研究一下该程序,以了解在调用带有某些数字输出的函数时幕后的情况。 假设compute_fibonacchi(3)。
众所周知,Python 是一种解释性和动态语言,这意味着您无需在使用变量之前声明变量。 这意味着在函数调用开始时,Python 解释器无法确定n将保留的值的类型。 当您使用某个整数值调用函数时,Python 会通过名为装箱和拆箱的过程自动为您进行类型推断。
在 Python 中,一切都是对象。 因此,当您输入1或hello时,Python 解释器将在内部将其转换为对象。 在许多在线材料中,此过程也称为拳击。 该过程可以可视化为:
那么当您将函数应用于对象时会发生什么呢? Python 解释器必须做一些额外的工作来推断类型并应用函数。 在一般意义上,下图说明了add函数在 Python 中的应用。 Python 是一种解释型语言,它在优化函数调用方面做得并不出色,但是可以使用 C 或 Cython 很好地优化它们:
这种装箱和拆箱不是免费的,需要花费宝贵的计算时间。 当这样的操作被循环执行多次时,效果变得更加显着。
在n = 20上运行时,以下程序在 IPython 笔记本上每个循环大约需要 1.8 微秒:
现在让我们将该程序重写为 Cython:
defcompute_fibonacchi_cython(int n):
cdefint a, b, intermediate, x
a, b= 1, 1
intermediate, x = 0, 0
for x in xrange(n):
intermediate = a
a = a+b
b = intermediate
return a
该程序每个循环花费64.5纳秒:
提示
尽管在此示例代码中提高速度非常重要,但这不是您将遇到的实际代码,因此您应始终记住首先在代码上运行分析器并确定需要优化的部分。 同样,在使用 Cython 时,开发人员应考虑在使用静态类型和灵活性之间进行权衡。 使用类型会降低灵活性,有时甚至会降低可读性。
通过删除xrange并改用for循环,可以进一步改进此代码。 当您对模块的所有组件/功能都满意并且没有错误后,用户可以将这些函数/过程存储在扩展名为.pyx的文件中。 这是 Cython 使用的扩展名。 将此代码与您的应用集成的下一步是在安装文件中添加信息。
在这里,出于说明目的,我们将代码存储在名为fib.pyx的文件中,并创建了一个构建该模块的安装文件:
from distutils.core import setup, Extension
from Cython.Build import cythonize
from Cython.Distutils import build_ext
setup(
ext_modules=[Extension('first', ['first.pyx'])],
cmdclass={'build_ext': build_ext}
)
在这里,请注意扩展名first 的名称与模块的名称完全匹配。 如果您无法保持相同的名称,则将收到一个神秘的错误:
多线程代码
您的应用可能会使用多线程代码。 由于全局解释器锁(GIL),Python 不适合多线程代码。 好消息是,在 Cython 中,您可以显式解锁 GIL,并使您的代码真正成为多线程。 只需在您的代码中放置一个with nogil:语句即可。 您以后可以在代码中使用with gil获取 GIL:
with nogil:
<The code block here>
function_name(args) with gil:
<function body>
NumPy 和 Cython
Cython 具有内置支持,可提供对 NumPy 数组的更快访问。 这些功能使 Cython 成为优化 NumPy 代码的理想人选。 在本节中,我们将研究用于计算欧式期权价格的代码,欧式期权是一种使用蒙特卡洛技术的金融工具。 不期望有金融知识; 但是,我们假设您对蒙特卡洛模拟有基本的了解:
defprice_european(strike = 100, S0 = 100, time = 1.0,
rate = 0.5, mu = 0.2, steps = 50,
N = 10000, option = "call"):
dt = time / steps
rand = np.random.standard_normal((steps + 1, N))
S = np.zeros((steps+1, N));
S[0] = S0
for t in range(1,steps+1):
S[t] = S[t-1] * np.exp((rate-0.5 * mu ** 2) * dt
+ mu * np.sqrt(dt) * rand[t])
price_call = (np.exp(-rate * time)
* np.sum(np.maximum(S[-1] - strike, 0))/N)
price_put = (np.exp(-rate * time)
* np.sum(np.maximum(strike - S[-1], 0))/N)
returnprice_call if option.upper() == "CALL" else price_put
以下是前面示例的 Cython 化代码:
import numpy as np
def price_european_cython(double strike = 100,doubleS0 = 100,
double time = 1.0, double rate = 0.5,
double mu = 0.2, int steps = 50,
long N = 10000, char* option = "call"):
cdef double dt = time / steps
cdefnp.ndarray rand = np.random.standard_normal((steps + 1, N))
cdefnp.ndarray S = np.zeros([steps+1, N], dtype=np.float)
#cdefnp.ndarrayprice_call = np.zeroes([steps+1,N], dtype=np.float)
S[0] = S0
for t in xrange(1,steps+1):
S[t] = S[t-1] * np.exp((rate-0.5 * mu ** 2) * dt
+ mu * np.sqrt(dt) * rand[t])
price_call = (np.exp(-rate * time)
* np.sum(np.maximum(S[-1] - strike, 0))/N)
price_put = (np.exp(-rate * time)
* np.sum(np.maximum(strike - S[-1], 0))/N)
return price_call if option.upper() == "CALL" else price_put
与此相关的安装文件如下所示:
from distutils.core import setup, Extension
from Cython.Build import cythonize
from Cython.Distutils import build_ext
import numpy.distutils.misc_util
include_dirs = numpy.distutils.misc_util.get_numpy_include_dirs()
setup(
name="numpy_first",
version="0.1",
ext_modules=[Extension('dynamic_BS_MC',
['dynamic_BS_MC.pyx'],
include_dirs = include_dirs)],
cmdclass={'build_ext': build_ext}
)
虽然通过 Cython 代码获得的加速效果非常好,并且您可能会倾向于在 Cython 中编写大多数代码,但建议仅将性能至关重要的部分转换为 Cython。 NumPy 在优化对数组的访问和执行更快的计算方面做得非常出色。 该代码可以视为描述该代码的理想候选者。 前面的代码有很多“松散的结果”,可以当作练习来解决 Python 中的性能问题,并在采用 Cython 方式之前先最佳地使用 NumPy。 由于 Python 的动态特性,盲目地对 NumPy 代码进行 Cython 化的速度提升可能不如具有真正问题的最优编写代码那样快。
最后,我们介绍在 Cython 中开发模块时应遵循的以下内容:
- 用纯 Python 编写代码并进行测试。
- 运行分析器并确定要关注的关键区域。
- 创建一个新模块以保存 Cython 代码(
<module_name>.pyx)。 - 将这些区域中的所有变量和循环索引转换为它们的 C 对应物。
- 使用以前的测试设置进行测试。
- 将扩展添加到安装文件中。
总结
在本章中,我们了解了如何将 Python 代码隐蔽到 Cython 中。 我们还研究了一些涉及 NumPy 数组的示例 Python 代码。 我们简要介绍了 Python 语言中装箱和拆箱的概念以及它们如何影响代码性能。 我们还说明了如何显式解锁臭名昭著的 GIL。 为了进一步深入研究 Cython,我们建议《Cython 编程学习手册》,Philip Herron,Packt Publishing。 在下一章中,您将了解 NumPy C API 以及如何使用它。
九、NumPy C-API 简介
NumPy 是一个通用库,旨在满足科学应用开发人员的大多数需求。 但是,随着应用的代码库和覆盖范围的增加,计算也随之增加,有时用户需要更具体的操作和优化的代码段。 我们已经展示了 NumPy 和 Python 如何具有诸如 F2PY 和 Cython 之类的工具来满足这些需求。 这些工具可能是将函数重写为本地编译代码以提高速度的绝佳选择。 但是在某些情况下(利用 C 库,例如 NAG 编写一些分析),您可能想做一些更根本的事情,例如为您自己的库专门创建新的数据结构。 这将要求您有权访问 Python 解释器中的低级控件。 在本章中,我们将研究如何使用 Python 及其扩展名 NumPy C-API 提供的 C-API 进行此操作。 C-API 本身是一个非常广泛的主题,可能需要一本书才能完全涵盖它。 在这里,我们将提供简短的介绍和示例,以帮助您开始使用 NumPy C-API。
本章将涉及的主题是:
- Python C-API 和 NumPy C-API
- 扩展模块的基本结构
- 一些特定于 NumPy 的 C-API 函数的简介
- 使用 C-API 创建函数
- 创建一个可调用的模块
- 通过 Python 解释器和其他模块使用模块
Python 和 NumPy C-API
我们使用的 Python 实现是 Python 解释器的基于 C 的实现。 NumPy 专用于此基于 C 的 Python 实现。 Python 的此实现带有 C-API,它是解释器的基础,并向其用户提供低级控制。 NumPy 通过提供丰富的 C-API 进一步增强了这一功能。
用 C/C++ 编写函数可以为开发人员提供灵活性,以利用这些语言提供的一些高级库。 但是,就必须在解析输入周围编写太多样板代码以构造返回值而言,代价显而易见。 此外,开发人员在引用/解引用对象时必须格外小心,因为这最终可能会导致讨厌的错误和内存泄漏。 随着 C-API 的不断发展,还存在代码未来兼容性的问题。 因此,如果开发人员想要迁移到更高版本的 Python,则他们可能需要为这些基于 C-API 的扩展进行大量维护工作。 由于这些困难,大多数开发人员选择尝试其他优化技术。 (例如 Cython 或 F2PY),然后再探索这条路径。 但是,在某些情况下,您可能想重用 C/C++ 中的其他现有库,这可能适合您的特定目的。 在这些情况下,最好为现有函数编写包装并公开 Python 项目。
接下来,我们将看一些示例代码,并在本章继续介绍时解释其关键函数和宏。 此处提供的代码与 Python 2.X 版本兼容,可能不适用于 Python 3.X。 但是,转换过程应该相似。
提示
开发人员可以尝试使用称为 cpychecker 的工具来检查模块中的引用计数时的常见错误。 请访问这里了解更多详细信息。
扩展模块的基本结构
用 C 编写的扩展模块将包含以下部分:
- 标头段,其中包含所有外部库和
Python.h - 初始化段,您可以在其中定义模块名称和 C 模块中的函数
- 方法结构数组,用于定义模块中的所有函数
- 一个实现部分,您在其中定义要公开的所有函数
标头段
标题片段是非常标准的,就像普通的 C 模块一样。 我们需要包括Python.h头文件,以使我们的 C 代码可以访问 C-API 的内部。 该文件位于<path_to_python>/include中。 我们将在示例代码中使用数组对象,因此我们也包含了numpy/arrayobject.h头文件。 我们不需要在此处指定头文件的完整路径,因为路径解析是在setup.py中处理的,我们将在后面进行介绍:
/*
Header Segment
*/
##include <Python.h>
##include <math.h>
##include <numpy/arrayobject.h>
Initialization Segment
初始化段
初始化段从以下内容开始:
- 调用
PyMODINIT_FUNC宏。 此宏在 Python 标头中定义,并且在开始定义模块之前总是会被调用。 - 下一行定义了初始化函数,并在加载该函数时由 Python 解释器调用。 函数名称必须为
init<module_name>格式,C 代码将要公开的模块和函数的名称。
该函数的主体包含对Py_InitModule3的调用,该调用定义模块的名称和模块中的函数。 该函数的一般结构如下:
(void)Py_InitModule3(name_of_module, method_array, Docstring)
对import_array()的最终调用是特定于 NumPy 的函数,如果您的函数正在使用 Numpy 数组对象,则需要此函数。 这样可以确保加载 C-API,以便如果您的 C++ 代码使用 C-API,则 API 表可用。 未能调用此函数和使用其他 NumPy API 函数将很可能导致分段错误错误。 建议您阅读 NumPy 文档中的import_array()和import_ufunc():
/*
Initialization module
*/
PyMODINIT_FUNC
initnumpy_api_demo(void)
{
(void)Py_InitModule3("numpy_api_demo", Api_methods,
"A demo to show Python and Numpy C-API");
import_array();
}
方法结构数组
在此部分中,您将定义模块将要公开给 Python 的方法数组。 我们在这里定义了两个函数以求其平方。 一种方法将普通的 Python double值作为输入,第二种方法对 Numpy 数组进行操作。 PyMethodDef结构可以在 C 中定义如下:
Struct PyMethodDef {
char *method_name;
PyCFunction method_function;
int method_flags;
char *method_docstring;
};
这是此结构的成员的描述:
method_name:函数的名称在此处。 这将是函数向 Python 解释器公开的名称。method_function:此变量保存在 Python 解释器中调用method_name时实际调用的 C 函数的名称。method_flags:这告诉解释器我们的函数正在使用三个签名中的哪个。 该标志的值通常为METH_VARARGS。 如果要允许关键字参数进入函数,可以将该标志与METH_KEYWORDS组合。 它也可以具有METH_NOARGS的值,这表明您不想接受任何参数。method_docstring:这是函数的文档字符串。
该结构需要以一个由NULL和 0 组成的标记终止,如以下示例所示:
/*
Method array structure definition
*/
static PyMethodDefApi_methods[] =
{
{"py_square_func", square_func, METH_VARARGS, "evaluate the squares"},
{"np_square", square_nparray_func, METH_VARARGS, "evaluates the square in numpy array"},
{NULL, NULL, 0, NULL}
};
实现部分
实现部分是最直接的部分。 这就是方法的 C 定义所要去的地方。 在这里,我们将研究两个函数来平方它们的输入值。 这些函数的复杂度保持在较低水平,以便您专注于方法的结构。
使用 Python C-API 创建数组平方函数
Python 函数将对自身的引用作为第一个参数,然后是赋予该函数的真实参数。 PyArg_ParseTuple函数用于将 Python 函数中的值解析为 C 函数中的局部变量。 在此函数中,我们将值强制转换为双精度,因此我们将d用作第二个参数。 您可以在这个页面上查看此函数接受的字符串的完整列表。
使用Py_Buildvalue返回计算的最终结果,它使用类似类型的格式字符串从您的答案中创建 Python 值。 我们在这里使用f表示浮点数,以证明对double和float的处理方式类似:
/*
Implementation of the actual C funtions
*/
static PyObject* square_func(PyObject* self, PyObject* args)
{
double value;
double answer;
/* parse the input, from python float to c double */
if (!PyArg_ParseTuple(args, "d", &value))
return NULL;
/* if the above function returns -1, an appropriate Python exception will
* have been set, and the function simply returns NULL
*/
answer = value*value;
return Py_BuildValue("f", answer);
}
使用 NumPy C-API 创建数组平方函数
在本节中,我们将创建一个函数以对 NumPy 数组的所有值求平方。 这里的目的是演示如何在 C 语言中获取 NumPy 数组,然后对其进行迭代。 在现实世界中,可以使用映射或通过向量化平方函数以更简单的方式完成此操作。 我们正在使用与O!格式字符串相同的PyArg_ParseTuple函数。 该格式字符串具有(object) [typeobject, PyObject *]签名,并以 Python 类型对象作为第一个参数。 用户应阅读官方 API 文档,以查看允许使用其他格式的字符串以及哪种字符串适合他们的需求:
注意
如果传递的值的类型不同,则引发TypeError。
以下代码段说明了如何使用PyArg_ParseTuple解析参数。
// Implementation of square of numpy array
static PyObject* square_nparray_func(PyObject* self, PyObject* args)
{
// variable declarations
PyArrayObject *in_array;
PyObject *out_array;
NpyIter *in_iter;
NpyIter *out_iter;
NpyIter_IterNextFunc *in_iternext;
NpyIter_IterNextFunc *out_iternext;
// Parse the argument tuple by specifying type "object" and putting the reference in in_array
if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &in_array))
return NULL;
......
......
下一步是创建一个数组以存储其输出值和迭代器,以便在 Numpy 数组上进行迭代。 请注意,创建对象时,每个步骤都有一个{handle failure}代码。 这是为了确保如果发生任何错误,我们可以通过调试来确定错误代码的位置:
//Construct the output from the new constructed input array
out_array = PyArray_NewLikeArray(in_array, NPY_ANYORDER, NULL, 0);
// Test it and if the input is nothing then just return nothing.
{handle failure}
// Create the iterators
in_iter = NpyIter_New(in_array, NPY_ITER_READONLY, NPY_KEEPORDER,
NPY_NO_CASTING, NULL);
// {handle failure}
out_iter = NpyIter_New((PyArrayObject *)out_array, NPY_ITER_READWRITE,
NPY_KEEPORDER, NPY_NO_CASTING, NULL);
{handle failure}
in_iternext = NpyIter_GetIterNext(in_iter, NULL);
out_iternext = NpyIter_GetIterNext(out_iter, NULL);
{handle failure}
double ** in_dataptr = (double **) NpyIter_GetDataPtrArray(in_iter);
double ** out_dataptr = (double **) NpyIter_GetDataPtrArray(out_iter);
A simple handle failure module is like
// {Start handling failure}
if (in_iter == NULL)
// remove the ref and return null
Py_XDECREF(out_array);
return NULL;
// {End handling failure}
看了前面的样板代码之后,我们终于来到了发生所有实际动作的部分。 那些熟悉 C++ 的人会发现迭代方法与向量迭代相似。 我们之前定义的in_iternext函数在这里派上用场,用于迭代 Numpy 数组。 在while循环之后,我们确保在两个迭代器上都调用了NpyIter_Deallocate,在输出数组上调用了Py_INCREF; 未能调用这些函数是导致内存泄漏的最常见错误类型。 内存泄漏问题通常非常微妙,通常在具有长时间运行的代码(例如服务或守护程序)时才会出现。 要抓住这些问题,不幸的是,没有比使用调试器更深入的方法容易的方法了。 有时,只需要编写几个printf语句即可输出总内存使用情况:
/* iterate over the arrays */
do {
out_dataptr =pow(**in_dataptr,2);
} while(in_iternext(in_iter) && out_iternext(out_iter));
/* clean up and return the result */
NpyIter_Deallocate(in_iter);
NpyIter_Deallocate(out_iter);
Py_INCREF(out_array);
return out_array;
构建和安装扩展模块
成功编写函数后,下一步是构建模块并在我们的 Python 模块中使用它。 setup.py文件看起来像以下代码片段:
from distutils.core import setup, Extension
import numpy
## define the extension module
demo_module = Extension('numpy_api_demo', sources=['numpy_api.c'],
include_dirs=[numpy.get_include()])
## run the setup
setup(ext_modules=[demo_module])
由于我们使用特定于 NumPy 的标头,因此我们需要在include_dirs变量中具有numpy.get_include函数。 要运行此安装文件,我们将使用一个熟悉的命令:
python setup.py build_ext -inplace
前面的命令将在目录中创建一个numpy_api_demo.pyd文件,供我们在 Python 解释器中使用。
为了测试我们的模块,我们将打开一个 Python 解释器测试,并尝试像我们对用 Python 编写的模块所做的一样,从该模块调用这些函数:
>>>import numpy_api_demo as npd
>>> import numpy as np
>>>npd.py_square_func(4)
>>> 16.0
>>> x = np.arange(0,10,1)
>>> y = npd.np_square(x)
总结
在本章中,我们向您介绍了另一种使用 Python 和 NumPy 提供的 C-API 优化或集成 C/C++ 代码的方法。 我们解释了该代码的基本结构以及其他示例代码,开发人员必须编写这些代码才能创建扩展模块。 之后,我们创建了两个函数,这些函数计算出一个数字的平方,并将该平方函数从math.h库映射到一个 Numpy 数组。 这里的目的是使您熟悉如何利用 C/C++ 编写的数字库,以最少的代码重写来创建自己的模块。 编写 C 代码的范围比这里描述的要广泛得多。 但是,我们希望本章使您有信心在需要时利用 C-API。