Skip to content

Latest commit

 

History

History
931 lines (684 loc) · 35.7 KB

File metadata and controls

931 lines (684 loc) · 35.7 KB

十四、C/C++ 中的扩展、系统调用和 C/C++ 库

现在我们知道了更多关于性能和多进程的内容,我们将解释另一个主题,它至少与性能相关,使用 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++模块。如果您真的需要最佳性能,那么几乎总是有高度优化的库可以满足您的需求。在某些情况下,需要使用本机 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

OSX

对于 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

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/C++

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

从 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...>

OSX

对于 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_pc_char_pc_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]

带内存管理的 Gotchas

除了明显的内存分配问题和可变和不可变对象的混合,还有一个奇怪的内存可变问题:

>>> 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

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的参数。但是,回到声明,您还可以指定要加载的库,而不是从Noneffi.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 重复VertexPoint示例:

>>> 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 还是 API?

和往常一样,不幸的是,这里有一些警告。到目前为止,这些示例部分使用了 ABI,它从库中加载二进制结构。对于标准 C 库,这通常是安全的;对于其他库,它通常不是。API 和 ABI 之间的区别在于后者在二进制级别调用函数,直接寻址内存,直接调用内存位置,并期望它们是函数。实际上,这是ffi.dlopenffi.cdef之间的区别。在这里,dlopen并不总是安全的,但cdef是安全的,因为它传递一个编译器,而不仅仅是猜测如何调用一个方法。

CFFI 还是 ctypes?

这真的取决于你在寻找什么。如果您有一个只需调用的 C 库,并且不需要任何特殊的东西,那么ctypes很可能是更好的选择。如果您正在编写自己的 C 库并尝试链接它,CFFI可能是一个更方便的选择。如果您不熟悉 C 编程语言,那么我肯定会推荐ctypes。或者,你会发现CFFI是一个更方便的选择。

本机 C/C++扩展

到目前为止,我们使用的库只向我们展示了如何在 Python 代码中访问 C/C++库。现在我们来看看故事的另一面,Python 中的 C/C++函数/模块是如何实际编写的,以及像cPicklecProfile这样的模块是如何创建的。

一个基本的例子

在我们真正开始编写和使用本机 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 我们有一个名为SpamExtension对象,它将基于spam.c

现在,让我们用 C 写一个函数,将所有的完美平方(2*23*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 多倍!

C 不是 Python——大小很重要

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 版本给出了不同的结果,195321423310543148825。这是由 C 中的整数溢出引起的。虽然 Python 数字基本上可以有任何大小,但在 C 中,常规数字的大小是固定的。得到多少取决于您使用的类型(intlong等等)和您的体系结构(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*

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_ParseTupleAndKeywordsPyArg_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},
};

注意这仍然支持普通参数,但现在也支持关键字参数。

C 不是 Python–错误是无声的或致命的

正如我们在前面的示例中所看到的,整数溢出并不是您通常会注意到的,不幸的是,没有很好的跨平台方法来捕获它们。然而,这些实际上是更容易处理的错误;最糟糕的通常是内存管理。在 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——但幸运的是,这是相同的基本原则。

从 C 调用 Python–处理复杂类型

我们已经了解了如何从 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

总结

在本章中,您学习了编写使用ctypesCFFI的代码的最重要方面,以及如何使用本机 C 扩展 Python 功能。这些主题可以足够广泛,足以填满书本,但您现在应该已经掌握了最重要的主题。即使您现在能够创建 C/C++扩展,我仍然建议您尽可能避免这些扩展。这是因为不小心很容易产生 bug。实际上,在内存管理方面,本章给出的示例中至少有一些可能包含 bug,并且在输入错误时可能会使 Python 解释器崩溃。不幸的是,这是 C 的一个副作用。一个小小的错误可以产生巨大的影响。

在构建本章中的示例时,您可能已经注意到我们使用了一个setup.py文件,并从setuptools库导入。下一章将介绍如何将代码打包到可安装的 Python 库中,并将其分发到 Python 包索引中。