现在我们知道了更多关于性能和多进程的内容,我们将解释另一个主题,它至少与性能相关,使用 C 和/或 C++扩展。
考虑 C/C++扩展有多种原因。现有库可用是一个重要的原因,但说实话,最重要的原因是性能。在第 12 章性能–跟踪并减少内存和 CPU 使用中,我们看到cProfile模块的速度大约是profile模块的 10 倍,这表明至少有一些 C 扩展比纯 Python 模块快。然而,本章将不太关注性能。这里的目标是与非 Python 库交互。任何性能改进都只是一个完全无意的副作用。
我们将在本章中讨论以下选项:
- 用于处理 Python 中的外部(C/C++)函数和数据的 C 类型
- CFFI(简称C 外功能接口,与
ctypes类似,但方式略有不同 - 编写本机 C/C++以扩展 Python
在开始本章之前,请务必注意,本章将需要一个与 Python 解释器配合良好的编译器。不幸的是,不同平台的情况有所不同。虽然对于大多数 Linux 发行版来说都很容易,但这在 Windows 上可能是一个很大的挑战。使用 OSX,只要安装了正确的工具,通常就足够简单了。
Python 手册中始终提供通用构建说明:
https://docs.python.org/3.5/extending/building.html
在几乎所有情况下,我倾向于说您不需要 C/C++模块。如果您真的需要最佳性能,那么几乎总是有高度优化的库可以满足您的需求。在某些情况下,需要使用本机 C/C++(或“不是 Python”)。如果您需要直接与具有特定计时的硬件通信,那么 Python 可能无法为您实现这一点。但是,通常情况下,这种通信应该留给负责特定计时的驾驶员。无论如何,即使您永远不会自己编写这些模块中的一个,在调试项目时,您可能仍然需要知道它们是如何工作的。
对于 Windows,一般建议使用 Visual Studio。特定版本取决于您的 Python 版本:
- Python 3.2 及更低版本:Microsoft Visual Studio 2008
- Python3.3 和 3.4:MicrosoftVisualStudio2010
- Python 3.5 和 3.6:Microsoft Visual Studio 2015
安装 VisualStudio 和编译 Python 模块的细节不在本书的范围之内。幸运的是,Python 文档中有一些文档可以帮助您入门:
https://docs.python.org/3.5/extending/windows.html
对于 Mac 来说,过程基本上是简单的,但是 OSX 有一些特定的技巧。
首先,通过 Mac 应用商店安装 Xcode。完成此操作后,您应该能够运行以下命令:
xcode-select --install接下来是有趣的部分。因为 OSX 附带了一个捆绑的 Python 版本(通常已经过时),所以我建议改为通过自制安装一个新的 Python 版本。有关安装 Homebrew 的最新说明,请访问 Homebrew 主页(http://brew.sh/ ),但安装自制软件的要点是以下命令:
# /usr/bin/ruby -e "$(curl -fsSL \
https://raw.githubusercontent.com/Homebrew/install/master/install)"之后,确保使用doctor命令检查所有设置是否正确:
# brew doctor完成所有这些操作后,只需通过自制程序安装 Python,并确保在执行脚本时使用该 Python 版本:
# brew install python3
# python3 –version
Python 3.5.1
which python3
/usr/local/bin/python3还要确保 Python 进程位于/usr/local/bin中,即自制版本。常规的 OSX 版本应该是/usr/bin/版本。
Linux/Unix 系统的安装在很大程度上取决于的发行版,但通常操作简单。
对于 Fedora、Red Hat、Centos 和其他使用yum作为包管理器的系统,请使用以下行:
# sudo yum install yum-utils
# sudo yum-builddep python3对于使用apt作为包管理器的 Debian、Ubuntu 和其他系统,请使用以下行:
# sudo apt-get build-dep python3.5请注意,Python3.5 还没有在任何地方都可用,因此您可能需要使用 Python3.4。
对于大多数系统来说,为了获得安装方面的帮助,沿着<operating system> python.h的路线进行网络搜索应该可以做到这一点。
ctypes库可以很容易地从 C 库调用函数,但您确实需要注意内存访问和数据类型。Python 通常在内存分配和类型转换方面非常宽松;C 绝对不是那么宽容。
尽管所有的平台都有一个标准的 C 库,但每个平台的位置和调用方法都不同。为了让大多数人都能轻松访问一个简单的环境,我将假设使用 Ubuntu(虚拟)机。如果您没有本地 Ubuntu 可用,那么可以在 Windows、Linux 和 OS X 上通过 VirtualBox 轻松运行它。
由于您通常希望在本机系统上运行示例,因此我们将首先展示从标准 C 库加载printf的基础知识。
从 Python 调用 C 函数的一个问题是默认库是特定于平台的。虽然以下示例在 Windows 系统上运行良好,但不会在其他平台上运行:
>>> import ctypes
>>> ctypes.cdll
<ctypes.LibraryLoader object at 0x...>
>>> libc = ctypes.cdll.msvcrt
>>> libc
<CDLL 'msvcrt', handle ... at ...>
>>> libc.printf
<_FuncPtr object at 0x...>由于这些限制,并非所有示例都可以在不需要手动编译的情况下适用于每个 Python 版本和发行版。从外部库函数调用函数的基本前提是简单地访问它们的名称作为ctypes导入的属性。然而,这是有区别的;在 Windows 上,模块通常是自动加载的,而在 Linux/Unix 系统上,则需要手动加载。
从 Linux/Unix 调用标准系统库确实需要手动加载,但幸运的是,这并不太复杂。从标准 C 库中获取printf函数非常简单:
>>> import ctypes
>>> ctypes.cdll
<ctypes.LibraryLoader object at 0x...>
>>> libc = ctypes.cdll.LoadLibrary('libc.so.6')
>>> libc
<CDLL 'libc.so.6', handle ... at ...>
>>> libc.printf
<_FuncPtr object at 0x...>对于 OS X,还需要显式的加载,但除此之外,它与普通 Linux/Unix 系统上的一切工作方式非常类似:
>>> import ctypes
>>> libc = ctypes.cdll.LoadLibrary('libc.dylib')
>>> libc
<CDLL 'libc.dylib', handle ... at 0x...>
>>> libc.printf
<_FuncPtr object at 0x...>除了加载库的方式之外,不幸的是还有更多不同之处,但这些示例至少为您提供了标准 C 库。它允许您直接从 C 实现中调用printf等函数。如果由于某种原因,无法加载正确的库,则始终存在ctypes.util.find_library函数。和往常一样,我建议显式声明优于隐式声明,但使用此函数可以使事情变得更简单。让我们举例说明在 OS X 系统上的运行:
>>> from ctypes import util
>>> from ctypes import cdll
>>> libc = cdll.LoadLibrary(util.find_library('libc'))
>>> libc
<CDLL '/usr/lib/libc.dylib', handle ... at 0x...>通过ctypes调用函数几乎和调用本机 Python 函数一样简单。值得注意的区别是参数和返回语句。这些变量应转换为本机 C 变量:
这些示例将假设您的范围内有前面段落中的一个示例中的libc。
>>> spam = ctypes.create_string_buffer(b'spam')
>>> ctypes.sizeof(spam)
5
>>> spam.raw
b'spam\x00'
>>> spam.value
b'spam'
>>> libc.printf(spam)
4
spam>>>正如您所看到的,要调用printf函数,您必须——而且我不能强调这一点——将您的值从 Python 显式转换为 C。虽然在没有这个的情况下,它看起来可能会工作,但它实际上不会:
>>> libc.printf(123)
segmentation fault (core dumped) python3记住使用第 11 章中的faulthandler模块调试——解决 bug来调试 SEGFULTS。
从该示例中需要注意的另一点是,ctypes.sizeof(spam)返回5而不是4。这是由 C 字符串所需的尾随空字符引起的。这在 C 字符串的原始属性中可见。没有它,printf函数将不知道字符串将在哪里结束。
要将其他类型(如整数)传递给libc函数,我们还必须使用一些转换。在某些情况下,它是可选的:
>>> format_string = ctypes.create_string_buffer(b'Number: %d\n')
>>> libc.printf(format_string, 123)
Number: 123
12
>>> x = ctypes.c_int(123)
>>> libc.printf(format_string, x)
Number: 123
12但并非在所有情况下都是如此,因此,明确建议您在所有情况下都显式转换值:
>>> format_string = ctypes.create_string_buffer(b'Number: %.3f\n')
>>> libc.printf(format_string, 123.45)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ctypes.ArgumentError: argument 2: <class 'TypeError'>: Don't know how to convert parameter 2
>>> x = ctypes.c_double(123.45)
>>> libc.printf(format_string, x)
Number: 123.450
16需要注意的是,即使这些值可用作本机 C 类型,它们仍然可以通过value属性进行修改:
>>> x = ctypes.c_double(123.45)
>>> x.value
123.45
>>> x.value = 456
>>> x
c_double(456.0)然而,如果原始对象是不可变的,则不是这种情况,这是一个非常重要的区别。create_string_buffer对象创建一个可变字符串对象,而c_wchar_p、c_char_p和c_void_p创建对实际 Python 字符串的引用。因为字符串在 Python 中是不可变的,所以这些值也是不可变的。您仍然可以更改value属性,但它只会分配一个新字符串。实际上,将其中一个传递给改变内部值的 C 函数会导致问题。
唯一应该转换成 C 的值是整数、字符串和字节,没有任何问题,但我个人建议您始终转换所有值,以便确定将获得哪种类型以及如何处理它。
我们已经看到,我们不能只将 Python 值传递给 C,但是如果我们需要更复杂的对象呢?也就是说,不仅仅是可直接转换为 C 的裸值,还有包含多个值的复杂对象。幸运的是,我们可以使用ctypes轻松创建(和访问)C 结构:
>>> class Spam(ctypes.Structure):
... _fields_ = [
... ('spam', ctypes.c_int),
... ('eggs', ctypes.c_double),
... ]
...>>> spam = Spam(123, 456.789)
>>> spam.spam
123
>>> spam.eggs
456.789在 Python 中,我们通常使用列表来表示对象的集合。这些都非常方便,因为您可以轻松地添加和删除值。在 C 语言中,默认的集合对象是数组,它只是一个固定大小的内存块。
块的大小(以字节为单位)由项数乘以类型大小决定。对于char而言,这是8位,因此如果您希望存储100字符,您将拥有100 * 8 bits = 800 bits = 100 bytes。
这就是它的全部——一块内存,从 C 接收到的唯一引用是指向内存块开始的内存地址的指针。由于指针确实有一个类型,char*在本例中,C 将知道在尝试访问其他项时要向前跳转多少字节。实际上,在尝试访问char数组中的项目 25 时,只需执行array_pointer + 25 * sizeof(char)即可。这有一个方便的快捷方式:array_pointer[25]。
请注意,C 不存储数组中的项目数,因此即使我们的数组只有 100 个项目,它也不会阻止我们执行array_pointer[1000]和读取其他(随机)内存。
如果你把所有这些都考虑进去,它肯定是可用的,但是错误很快就会发生,而 C 是不可原谅的。没有警告,只是崩溃和行为异常的代码。除此之外,让我们看看使用ctypes声明数组有多容易:
>>> TenNumbers = 10 * ctypes.c_double
>>> numbers = TenNumbers()
>>> numbers[0]
0.0正如您所看到的,由于大小固定,并且需要在使用类型之前声明类型,因此它的使用有点笨拙。但是,它的功能与您预期的一样,默认情况下,这些值初始化为零。显然,这也可以与前面讨论的结构相结合:
>>> Spams = 5 * Spam
>>> spams = Spams()
>>> spams[0].eggs = 123.456
>>> spams
<__main__.Spam_Array_5 object at 0x...>
>>> spams[0]
<__main__.Spam object at 0x...>
>>> spams[0].eggs
123.456
>>> spams[0].spam
0即使您不能简单地附加到这些数组来调整它们的大小,但实际上它们可以通过一些约束来调整大小。首先,新阵列需要大于原阵列。其次,需要以字节而不是项目来指定大小。为了说明这一点,我们有以下示例:
>>> TenNumbers = 10 * ctypes.c_double
>>> numbers = TenNumbers()
>>> ctypes.resize(numbers, 11 * ctypes.sizeof(ctypes.c_double))
>>> ctypes.resize(numbers, 10 * ctypes.sizeof(ctypes.c_double))
>>> ctypes.resize(numbers, 9 * ctypes.sizeof(ctypes.c_double))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: minimum size is 80
>>> numbers[:5] = range(5)
>>> numbers[:]
[0.0, 1.0, 2.0, 3.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0]除了明显的内存分配问题和可变和不可变对象的混合,还有一个奇怪的内存可变问题:
>>> class Point(ctypes.Structure):
... _fields_ = ('x', ctypes.c_int), ('y', ctypes.c_int)
...
>>> class Vertex(ctypes.Structure):
... _fields_ = ('a', Point), ('b', Point), ('c', Point)
...
>>> v = Vertex()
>>> v.a = Point(0, 1)
>>> v.b = Point(2, 3)
>>> v.c = Point(4, 5)
>>> v.a.x, v.a.y, v.b.x, v.b.y, v.c.x, v.c.y
(0, 1, 2, 3, 4, 5)
>>> v.a, v.b, v.c = v.b, v.c, v.a
>>> v.a.x, v.a.y, v.b.x, v.b.y, v.c.x, v.c.y
(2, 3, 4, 5, 2, 3)
>>> v.a.x = 123
>>> v.a.x, v.a.y, v.b.x, v.b.y, v.c.x, v.c.y
(123, 3, 4, 5, 2, 3)为什么我们没有得到2, 3, 4, 5, 0, 1?问题是这些对象被复制到临时缓冲区变量。同时,该对象的值正在更改,因为它在内部包含单独的对象。之后,对象被传回,值已经改变,给出了不正确的结果。
CFFI库提供的选项与ctypes非常相似,但更直接。与ctypes库不同,C 编译器确实是CFFI所必需的。这样就有机会以一种非常简单的方式直接调用您的 C 编译器:
>>> import cffi
>>> ffi = cffi.FFI()
>>> ffi.cdef('int printf(const char* format, ...);')
>>> libc = ffi.dlopen(None)
>>> arg = ffi.new('char[]', b'spam')
>>> libc.printf(arg)
4
spam>>>好吧…看起来有点奇怪,对吧?我们必须定义printf函数的外观,并使用有效的 C 类型声明指定printf的参数。但是,回到声明,您还可以指定要加载的库,而不是从None到ffi.dlopen。如果您还记得ctypes.util.find_library功能,您可以在本例中再次使用该功能:
>>> from ctypes import util
>>> import cffi
>>> libc = ffi.dlopen(util.find_library('libc'))
>>> ffi.printf
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'FFI' object has no attribute 'printf'但它仍然不能让你轻易地得到它的定义。函数定义仍然是所必需的,以确保一切按您所希望的方式运行。
CFFI定义与ctypes定义有些相似,但不是让 Python 模拟 C,而是可以从 Python 访问的普通 C。实际上,这只是一个小小的句法差异。而ctypes是一个从 Python 访问 C 的库,同时尽可能接近 Python 语法,CFFI使用纯 C 语法访问 C 系统,这实际上消除了对有 C 经验的人的一些困惑。我个人觉得CFFI更容易使用,因为我知道实际发生了什么,而我对ctypes并不总是百分之百的肯定。让我们用 CFFI 重复Vertex和Point示例:
>>> import cffi
>>> ffi = cffi.FFI()
>>> ffi.cdef('''
... typedef struct {
... int x;
... int y;
... } point;
...
... typedef struct {
... point a;
... point b;
... point c;
... } vertex;
... ''')
>>> vertices = ffi.new('vertex[]', 5)
>>> v = vertices[0]
>>> v.a.x = 1
>>> v.a.y = 2
>>> v.b.x = 3
>>> v.b.y = 4
>>> v.c.x = 5
>>> v.c.y = 6
>>> v.a.x, v.a.y, v.b.x, v.b.y, v.c.x, v.c.y
(1, 2, 3, 4, 5, 6)
v.a, v.b, v.c = v.b, v.c, v.a
v.a.x, v.a.y, v.b.x, v.b.y, v.c.x, v.c.y
>>> v.a, v.b, v.c = v.b, v.c, v.a
>>> v.a.x, v.a.y, v.b.x, v.b.y, v.c.x, v.c.y
(3, 4, 5, 6, 3, 4)正如您所看到的,可变的变量问题仍然存在,但代码同样可用。
对于CFFI,新变量的分配内存几乎微不足道。上一段向您展示了一个数组分配的示例;现在让我们看看数组定义的可能性:
>>> import cffi
>>> ffi = cffi.FFI()
>>> x = ffi.new('int[10]')
>>> y = ffi.new('int[]', 10)
>>> x[0:10] = range(10)
>>> y[0:10] = range(10, 0, -1)
>>> list(x)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(y)
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]在这种情况下,您可能想知道为什么切片同时包含开始和停止。这实际上是对CFFI的要求。虽然不总是有问题,但还是有点烦人。然而,目前这是不可避免的。
和往常一样,不幸的是,这里有一些警告。到目前为止,这些示例部分使用了 ABI,它从库中加载二进制结构。对于标准 C 库,这通常是安全的;对于其他库,它通常不是。API 和 ABI 之间的区别在于后者在二进制级别调用函数,直接寻址内存,直接调用内存位置,并期望它们是函数。实际上,这是ffi.dlopen和ffi.cdef之间的区别。在这里,dlopen并不总是安全的,但cdef是安全的,因为它传递一个编译器,而不仅仅是猜测如何调用一个方法。
这真的取决于你在寻找什么。如果您有一个只需调用的 C 库,并且不需要任何特殊的东西,那么ctypes很可能是更好的选择。如果您正在编写自己的 C 库并尝试链接它,CFFI可能是一个更方便的选择。如果您不熟悉 C 编程语言,那么我肯定会推荐ctypes。或者,你会发现CFFI是一个更方便的选择。
到目前为止,我们使用的库只向我们展示了如何在 Python 代码中访问 C/C++库。现在我们来看看故事的另一面,Python 中的 C/C++函数/模块是如何实际编写的,以及像cPickle和cProfile这样的模块是如何创建的。
在我们真正开始编写和使用本机 C/C++扩展之前,我们有几个先决条件。首先,我们需要编译器和 Python 头;本章开头的说明本应为我们解决这一问题。之后,我们需要告诉 Python 要编译什么。setuptools包主要负责这一点,但我们确实需要创建一个setup.py文件:
import setuptools
spam = setuptools.Extension('spam', sources=['spam.c'])
setuptools.setup(
name='Spam',
version='1.0',
ext_modules=[spam],
)这告诉 Python 我们有一个名为Spam的Extension对象,它将基于spam.c。
现在,让我们用 C 写一个函数,将所有的完美平方(2*2、3*3等等)求和到一个给定的数字。Python 代码如下所示:
def sum_of_squares(n):
sum = 0
for i in range(n):
if i * i < n:
sum += i * i
else:
break
return sum此代码的原始 C 版本如下所示:
long sum_of_squares(long n){
long sum = 0;
/* The actual summing code */
for(int i=0; i<n; i++){
if((i * i) < n){
sum += i * i;
}else{
break;
}
}
return sum;
}Python C 版本如下所示:
#include <Python.h>
static PyObject* spam_sum_of_squares(PyObject *self, PyObject
*args){
/* Declare the variables */
int n;
int sum = 0;
/* Parse the arguments */
if(!PyArg_ParseTuple(args, "i", &n)){
return NULL;
}
/* The actual summing code */
for(int i=0; i<n; i++){
if((i * i) < n){
sum += i * i;
}else{
break;
}
}
/* Return the number but convert it to a Python object first
*/
return PyLong_FromLong(sum);
}
static PyMethodDef spam_methods[] = {
/* Register the function */
{"sum_of_squares", spam_sum_of_squares, METH_VARARGS,
"Sum the perfect squares below n"},
/* Indicate the end of the list */
{NULL, NULL, 0, NULL},
};
static struct PyModuleDef spam_module = {
PyModuleDef_HEAD_INIT,
"spam", /* Module name */
NULL, /* Module documentation */
-1, /* Module state, -1 means global. This parameter is
for sub-interpreters */
spam_methods,
};
/* Initialize the module */
PyMODINIT_FUNC PyInit_spam(void){
return PyModule_Create(&spam_module);
}这看起来很复杂,但其实没那么难。在这种情况下会有很多开销,因为我们只有一个函数。通常,您会有几个函数,在这种情况下,您只需要扩展spam_methods数组并创建函数。下一段将更详细地解释代码,但首先让我们看看如何运行第一个示例。我们需要构建并安装模块:
# python setup.py build install
running build
running build_ext
running install
running install_lib
running install_egg_info
Removing lib/python3.5/site-packages/Spam-1.0-py3.5.egg-info
Writing lib/python3.5/site-packages/Spam-1.0-py3.5.egg-info现在,让我们创建一个小测试脚本来计算 Python 版本和 C 版本之间的时间差:
import sys
import spam
import timeit
def sum_of_squares(n):
sum = 0
for i in range(n):
if i * i < n:
sum += i * i
else:
break
return sum
if __name__ == '__main__':
c = int(sys.argv[1])
n = int(sys.argv[2])
print('%d executions with n: %d' % (c, n))
print('C sum of squares: %d took %.3f seconds' % (
spam.sum_of_squares(n),
timeit.timeit('spam.sum_of_squares(n)', number=c,
globals=globals()),
))
print('Python sum of squares: %d took %.3f seconds' % (
sum_of_squares(n),
timeit.timeit('sum_of_squares(n)', number=c,
globals=globals()),
))现在让我们来执行它:
# python3 test_spam.py 10000 1000000
10000 executions with n: 1000000
C sum of squares: 332833500 took 0.008 seconds
Python sum of squares: 332833500 took 1.778 seconds完美的结果完全相同,但速度快了 200 多倍!
Python 语言使编程变得如此简单,以至于有时您可能会忘记底层的数据结构;有了 C,你就负担不起了。仅以上一章中的示例为例,但参数不同:
# python3 test_spam.py 1000 10000000
1000 executions with n: 10000000
C sum of squares: 1953214233 took 0.002 seconds
Python sum of squares: 10543148825 took 0.558 seconds速度还是很快,但是数字怎么了?Python 和 C 版本给出了不同的结果,1953214233和10543148825。这是由 C 中的整数溢出引起的。虽然 Python 数字基本上可以有任何大小,但在 C 中,常规数字的大小是固定的。得到多少取决于您使用的类型(int、long等等)和您的体系结构(32 位、64 位等等),但这绝对是需要小心的。在某些情况下,速度可能会快几百倍,但如果结果不正确,这是毫无意义的。
当然,我们可以稍微加大尺寸。这使它变得更好:
static PyObject* spam_sum_of_squares(PyObject *self, PyObject *args){
/* Declare the variables */
unsigned long long int n;
unsigned long long int sum = 0;
/* Parse the arguments */
if(!PyArg_ParseTuple(args, "K", &n)){
return NULL;
}
/* The actual summing code */
for(unsigned long long int i=0; i<n; i++){
if((i * i) < n){
sum += i * i;
}else{
break;
}
}
/* Return the number but convert it to a Python object first */
return PyLong_FromUnsignedLongLong(sum);
}如果我们现在测试它,我们会发现它非常有效:
# python3 test_spam.py 1000 100000001000 executions with n: 10000000
C sum of squares: 10543148825 took 0.002 seconds
Python sum of squares: 10543148825 took 0.635 seconds除非我们让这个数字更大:
# python3 test_spam.py 1 100000000000000 ~/Dropbox/Mastering Python/code/h14
1 executions with n: 100000000000000
C sum of squares: 1291890006563070912 took 0.006 seconds
Python sum of squares: 333333283333335000000 took 2.081 seconds那么你怎么能解决这个问题呢?简单的答案是你不能。复杂的答案是,如果您使用不同的数据类型来存储数据,您可以这样做。C 语言本身没有 Python 所具有的“大数字支持”。Python 通过组合实际内存中的几个正则数来支持无限大的数。在 C 语言中,没有通用的规定,因此根本没有简单的方法来实现这一点。但我们可以检查错误:
static unsigned long long int get_number_from_object(int* overflow, PyObject* some_very_large_number){
return PyLong_AsLongLongAndOverflow(sum, overflow);
}注意,这只适用于PyObject*,这意味着它不适用于内部 C 溢出。但是,当然,您可以将原始 Python 保留很长时间,并在其上执行操作。因此,在 C 语言中,您可以不费吹灰之力地获得大量支持。
我们已经从我们的示例中看到了结果,但是如果您不熟悉 Python C API,您可能会对函数参数的外观感到困惑。spam_sum_of_squares中的基本计算与常规 Csum_of_squares函数相同,但存在一些小差异。首先,使用 Python C API 的函数的类型定义应该如下所示:
static PyObject* spam_sum_of_squares(PyObject *self, PyObject
*args)这意味着功能是static。静态函数只能从编译器中的同一翻译单元调用。这有效地导致了一个无法与其他模块链接的函数,这允许编译器进一步优化。由于 C 中的函数默认为全局函数,因此这对于防止冲突非常有用。但是,为了确保这一点,我们在函数名前面加了一个前缀spam_,表示该函数来自spam模块。
注意不要将此处的单词static与变量前面的static混淆。他们是完全不同的野兽。static变量是指将在程序的整个运行时存在的变量,而不仅仅是函数的运行时。
PyObject类型是 Python 数据类型的基本类型,这意味着所有 Python 对象都可以转换为PyObject*(指针PyObject。实际上,它只告诉编译器需要什么类型的属性,可以在以后用于类型识别和内存管理。与直接访问PyObject*不同,通常最好使用可用的宏,如Py_TYPE(some_object)。在内部,这扩展到(((PyObject*)(o))->ob_type),这就是为什么宏通常是一个更好的主意。除了无法阅读之外,打字错误也很容易发生。
属性列表很长,很大程度上取决于对象的类型。对于这些,我想参考 Python 文档:
https://docs.python.org/3/c-api/typeobj.html
整个 pythoncapi 可以自己写一本书,但幸运的是,它在 Python 手册中有很好的文档记录。另一方面,用法可能不那么明显。
对于常规 C 和 Python,可以显式指定参数,因为变量大小的参数在 C 中有点棘手。这是因为它们需要单独解析。PyObject* args是对包含实际值的对象的引用。要解析这些变量,您需要知道预期的变量数量和类型。在本例中,我们使用了PyArg_ParseTuple函数,该函数仅将参数解析为位置参数,但使用PyArg_ParseTupleAndKeywords或PyArg_VaParseTupleAndKeywords也很容易解析命名参数。后两种方法之间的区别在于,第一种方法使用数量可变的参数来指定目标,而后一种方法使用va_list将值设置为。但首先,让我们分析实际示例中的代码:
if(!PyArg_ParseTuple(args, "i", &n)){
return NULL;
}我们知道,args是包含对实际参数的引用的对象。"i"是一个格式字符串,在本例中,它将尝试解析单个整数。并且&n告诉函数将值存储在n变量的存储器地址。
格式字符串是这里的重要部分。根据字符的不同,您会得到不同的数据类型,但有很多;i指定一个正则整数,s将您的变量转换为 c 字符串(实际上是一个char*,它是一个以 null 结尾的字符数组)。应该注意的是,幸运的是,这个函数足够聪明,可以考虑溢出。
解析多个参数非常相似;您只需向格式字符串和多个目标变量添加多个字符:
PyObject* callback;
int n;
/* Parse the arguments */
if(!PyArg_ParseTuple(args, "Oi", &callback, &n)){
return NULL;
}带有关键字参数的版本与此类似,但需要对代码进行更多更改,因为需要通知方法列表函数使用关键字参数。否则,kwargs参数永远不会到达:
static PyObject* function(
PyObject *self,
PyObject *args,
PyObject *kwargs){
/* Declare the variables */
int sum = 0;
PyObject* callback;
int n;
static char* keywords[] = {"callback", "n", NULL};
/* Parse the arguments */
if(!PyArg_ParseTupleAndKeywords(args, kwargs, "Oi", keywords,
&callback, &n)){
return NULL;
}
Py_RETURN_NONE;
}
static PyMethodDef methods[] = {
/* Register the function with kwargs */
{"function", function, METH_VARARGS | METH_KEYWORDS,
"Some kwargs function"},
/* Indicate the end of the list */
{NULL, NULL, 0, NULL},
};注意这仍然支持普通参数,但现在也支持关键字参数。
正如我们在前面的示例中所看到的,整数溢出并不是您通常会注意到的,不幸的是,没有很好的跨平台方法来捕获它们。然而,这些实际上是更容易处理的错误;最糟糕的通常是内存管理。在 Python 中,如果出现错误,您将得到一个可以捕获的异常。但是使用 C 语言,您无法真正优雅地处理它。以零除为例:
# python3 -c '1/0'
Traceback (most recent call last):
File "<string>", line 1, in <module>
ZeroDivisionError: division by zero这很简单,可以用try: ... except ZeroDivisionError: ...来理解。另一方面,使用 C 时,如果出现错误,则会终止整个过程。但是调试 C 代码是 C 编译器有调试器的目的,为了找到错误的原因,您可以使用第 11 章中讨论的faulthandler模块调试–解决错误。现在,让我们看看如何正确地从 C 抛出错误。让我们使用前面的spam模块,但为简洁起见,我们将省略 C 代码的其余部分:
static PyObject* spam_eggs(PyObject *self, PyObject *args){
PyErr_SetString(PyExc_RuntimeError, "Too many eggs!");
return NULL;
}
static PyMethodDef spam_methods[] = {
/* Register the function */
{"eggs", spam_eggs, METH_VARARGS,
"Count the eggs"},
/* Indicate the end of the list */
{NULL, NULL, 0, NULL},
};执行情况如下:
# python3 setup.py clean build install
...
# python3 -c 'import spam; spam.eggs()'
Traceback (most recent call last):
File "<string>", line 1, in <module>
RuntimeError: Too many eggs!语法略有不同PyErr_SetString而不是raise——但幸运的是,这是相同的基本原则。
我们已经了解了如何从 Python 调用 C 函数,但现在让我们尝试从 C 调用 Python。我们不使用现成的sum函数,而是使用回调和处理任何类型的 iterable 来构建自己的函数。虽然这听起来很简单,但它实际上需要一些类型干预,因为您只能将PyObject*作为参数。这与简单类型(如整数、字符和字符串)相反,这些类型会立即转换为本机 Python 版本:
static PyObject* spam_sum(PyObject* self, PyObject* args){
/* Declare all variables, note that the values for sum and
* callback are defaults in the case these arguments are not
* specified */
long long int sum = 0;
int overflow = 0;
PyObject* iterator;
PyObject* iterable;
PyObject* callback = NULL;
PyObject* value;
PyObject* item;
/* Now we parse a PyObject* followed by, optionally
* (the | character), a PyObject* and a long long int */
if(!PyArg_ParseTuple(args, "O|OL", &iterable, &callback,
&sum)){
return NULL;
}
/* See if we can create an iterator from the iterable. This is
* effectively the same as doing iter(iterable) in Python */
iterator = PyObject_GetIter(iterable);
if(iterator == NULL){
PyErr_SetString(PyExc_TypeError,
"Argument is not iterable");
return NULL;
}
/* Check if the callback exists or wasn't specified. If it was
* specified check whether it's callable or not */
if(callback != NULL && !PyCallable_Check(callback)){
PyErr_SetString(PyExc_TypeError,
"Callback is not callable");
return NULL;
}
/* Loop through all items of the iterable */
while((item = PyIter_Next(iterator))){
/* If we have a callback available, call it. Otherwise
* just return the item as the value */
if(callback == NULL){
value = item;
}else{
value = PyObject_CallFunction(callback, "O", item);
}
/* Add the value to sum and check for overflows */
sum += PyLong_AsLongLongAndOverflow(value, &overflow);
if(overflow > 0){
PyErr_SetString(PyExc_RuntimeError,
"Integer overflow");
return NULL;
}else if(overflow < 0){
PyErr_SetString(PyExc_RuntimeError,
"Integer underflow");
return NULL;
}
/* If we were indeed using the callback, decrease the
* reference count to the value because it is a separate
* object now */
if(callback != NULL){
Py_DECREF(value);
}
Py_DECREF(item);
}
Py_DECREF(iterator);
return PyLong_FromLongLong(sum);
}确保注意到PyDECREF调用,这确保不会泄漏这些对象。没有它们,对象将继续使用,Python 解释器将无法清除它们。
此函数可通过三种不同的方式调用:
>>> import spam
>>> x = range(10)
>>> spam.sum(x)
45
>>> spam.sum(x, lambda y: y + 5)
95
>>> spam.sum(x, lambda y: y + 5, 5)
100另一个重要的问题是,即使我们在转换为long long int时捕捉到溢出错误,此代码仍然不安全。如果我们将两个非常大的数字相加(接近long long int极限),我们仍然会有一个溢出:
>>> import spam
>>> n = (2 ** 63) - 1
>>> x = n,
>>> spam.sum(x)
9223372036854775807
>>> x = n, n
>>> spam.sum(x)
-2在本章中,您学习了编写使用ctypes、CFFI的代码的最重要方面,以及如何使用本机 C 扩展 Python 功能。这些主题可以足够广泛,足以填满书本,但您现在应该已经掌握了最重要的主题。即使您现在能够创建 C/C++扩展,我仍然建议您尽可能避免这些扩展。这是因为不小心很容易产生 bug。实际上,在内存管理方面,本章给出的示例中至少有一些可能包含 bug,并且在输入错误时可能会使 Python 解释器崩溃。不幸的是,这是 C 的一个副作用。一个小小的错误可以产生巨大的影响。
在构建本章中的示例时,您可能已经注意到我们使用了一个setup.py文件,并从setuptools库导入。下一章将介绍如何将代码打包到可安装的 Python 库中,并将其分发到 Python 包索引中。