Cython-编程学习指南第二版-全-

news/2025/9/18 12:48:07/文章来源:https://www.cnblogs.com/apachecn/p/19098533

Cython 编程学习指南第二版(全)

原文:zh.annas-archive.org/md5/0bc691743f26fcdcabcb6840b706a834

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Cython 是一个工具,它使得编写 Python 的原生扩展变得和编写 Python 代码一样简单。对于那些不知道的人来说,你可以将 Python 模块实现为纯 C 代码,这在所有意图和目的上看起来和任何 Python 代码一样。这在实现 Python 中的模块时是必需的,例如,内置的 zip 模块在底层使用原生 zlib。这样做对于 Python 标准库模块部分是有意义的,尽管对于大多数 Python 用户来说,如果可能的话,编写原生模块应该是最后的手段。

编写原生模块很困难,需要掌握如何正确使用垃圾回收器调用以避免内存泄漏的先验知识。它还要求了解 GIL 的使用方式,这取决于你使用的是 CPython 还是 PyPy。它还要求了解模块结构和 Python 运行时内部的参数传递。因此,当需要时,这不是一个简单的过程。Cython 让我们能够编写和操作原生代码,而无需了解任何关于 Python 运行时的知识。我们可以编写几乎纯 Python 代码,恰好可以让我们操作 C/C++类型和类。我们可以从原生代码和 Python 代码之间来回调用。

更重要的是,Cython 消除了复杂性和内在性,让程序员能够专注于解决问题。

本书涵盖的内容

第一章,Cython 不会咬人,介绍了核心概念并演示了 Cython "Hello World"。它讨论了类型和类型转换。

第二章,理解 Cython,作为本书的参考。我们查看自定义 C 类型和函数指针。使用这些,我们将能够直接从 C 代码中成功使用 Python 模块。

第三章,扩展应用程序,使用前几章中的所有内容,使用 Python 而不是 C/C++编写原生 Tmux 命令。

第四章,调试 Cython,使用 cygdb 包装器在 gdb 上调试 Cython 代码。

第五章,高级 Cython,介绍了 Cython 如何与 C++类和模板很好地工作。一般来说,它还涵盖了 Cython 的注意事项。

第六章,进一步阅读,简要介绍了相关项目和新的学习资源。

你需要这本书的内容

对于这本书,我使用了我的 MacBook 和一个 Ubuntu 虚拟机(在 Mac OS X 上,GDB 对于调试来说太老了)。在 Mac OS X 上,你需要以下内容:

  • Xcode

  • Cython

  • GCC/Clang

  • Make

  • Python

  • Python con g

  • Python distutils

在 Ubuntu/Debian 上,你可以使用以下命令安装所有内容:

$ sudo apt-get install build-essential gdb cython

我将在引言中介绍这一点,但只要您有一个工作的 C 编译器和 Python,以及安装了 Python 库和头文件,您将拥有 Cython 所需的一切。

本书面向对象

这本书是为喜欢使用 Python 的 C/C++开发者和想要将原生 C/C++扩展到 Python 的用户所写的。作为读者,你可以期待看到如何使用 Cython 开发应用程序,重点是扩展现有系统,并得到如何接近这一目标的帮助。

扩展遗留系统可能很困难,但回报可能很大。考虑在 C 中实现低级线程感知或 I/O 敏感的操作,并保持由 Python 处理和提供的逻辑。这种开发模式可以证明是高效的,并且对开发时间的回报很大,尤其是在 C 应用程序中。

它还允许系统状态或逻辑的快速开发。无需担心在 C 中进行长时间数据转换算法来执行小任务,然后再需要全部更改它们。

惯例

在这本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“首选的做法是使用pip。”

代码块设置如下:

#include <stdio.h>int AddFunction(int a, int b) {printf("look we are within your c code!\n");return a + b;
}

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

>>> pyximport.install()
(None, <pyximport.pyximport.PyxImporter object at 0x102fba4d0>)
>>> import helloworld
Hello World from cython!

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“点击下一步按钮将您带到下一屏幕。”

注意

警告或重要注意事项如下所示。

小贴士

小贴士和技巧如下所示。

读者反馈

我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要发送给我们一般性的反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。

如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。

客户支持

现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com下载示例代码文件,适用于您购买的所有 Packt 出版书籍。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的 SUPPORT 标签上。

  3. 点击代码下载与勘误。

  4. 在搜索框中输入书籍名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击代码下载。

下载文件后,请确保使用最新版本的以下软件解压缩或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者和我们提供有价值内容的能力方面的帮助。

问题

如果您在这本书的任何方面遇到问题,您可以通过<questions@packtpub.com>与我们联系,我们将尽力解决问题。

第一章. Cython 不会咬人

Cython 远不止是一种编程语言。其起源可以追溯到 SAGE,这是一个数学软件包,其中它被用来提高涉及矩阵等数学计算的性能。更普遍地说,我倾向于将 Cython 视为 SWIG 的替代品,用于生成非常好的 Python 到本地代码的绑定。

语言绑定已经存在多年,SWIG 是最早和最好的用于为多种语言生成绑定的工具之一。Cython 仅生成 Python 代码的绑定,这种单一目的的方法意味着它生成的 Python 绑定是您能得到的最好的,除了手动完成之外,这应该只在您是 Python 核心开发者的情况下尝试。

对我来说,通过生成语言绑定来控制遗留软件是一种很好的重用任何软件包的方法。考虑一个用 C/C++ 编写的遗留应用程序。为仪表板或消息总线添加高级现代功能,如网络服务器,并不是一件简单的事情。更重要的是,Python 拥有数千个经过开发、测试并由人们长期使用、能够完成这些任务的软件包。利用所有这些代码不是很好吗?使用 Cython,我们确实可以做到这一点,我将在过程中通过大量的示例代码来展示方法。

本章将致力于 Cython 的核心概念,包括编译,并应为所有 Cython 核心概念提供一个坚实的参考和介绍。

在本章中,我们将涵盖:

  • 安装 Cython

  • 入门 - Hello World

  • 使用 distutils 与 Cython

  • 从 Python 调用 C 函数

  • 类型转换

安装 Cython

由于 Cython 是一种编程语言,我们必须安装其相应的编译器,而这个编译器恰好被恰当地命名为 Cython

安装 Cython 有许多不同的方法。首选的方法是使用 pip

$ pip install Cython

这应该在 Linux 和 Mac 上都适用。或者,您可以使用您的 Linux 发行版的包管理器来安装 Cython:

$ yum install cython     # will work on Fedora and Centos
$ apt-get install cython # will work on Debian based systems.

对于 Windows 系统,尽管有众多选项可用,但遵循本维基是保持最新状态的最安全选项:wiki.cython.org/InstallingOnWindows

Emacs 模式

Cython 有一个可用的 emacs 模式。尽管语法几乎与 Python 相同,但与简单地使用 Python-mode 存在着冲突。您可以从 Cython 源代码(在 Tools 目录内)中获取 cython-mode.el。在 emacs 中安装包的首选方法是使用包仓库,如 MELPA

要将包仓库添加到 emacs,打开您的 ~/.emacs 配置文件并添加:

(when (>= emacs-major-version 24)(require 'package)(add-to-list'package-archives'("melpa" . "http://melpa.org/packages/")t)(package-initialize))

一旦添加此内容并重新加载您的配置以安装 Cython 模式,您只需运行:

'M-x package-install RET cython-mode'

一旦安装,您可以通过将以下内容添加到您的 emacs 配置文件中来激活模式:

(require 'cython-mode)

您可以随时手动激活该模式,方法如下:

'M-x cython-mode RET'

获取代码示例

在整本书中,我打算展示一些易于消化的真实示例,以帮助你了解你可以使用 Cython 实现的不同事情。要访问和下载使用的代码,请克隆此存储库:

$ git clone git://github.com/redbrain/cython-book.git

入门 – Hello World

当你运行 Hello World 程序时,你会看到 Cython 生成原生 Python 模块。因此,运行任何 Cython 代码时,你将通过 Python 的模块导入来引用它。让我们构建这个模块:

$ cd cython-book/chapter1/helloworld
$ make

现在你应该已经创建了 helloworld.so!这是一个与 Cython 源代码文件同名的 Cython 模块。在共享对象模块的同一目录下,你可以通过运行相应的 Python 导入来调用此代码:

$ python
Python 2.7.3 (default, Aug  1 2012, 05:16:07)
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import helloworld
Hello World from cython!

通过打开 helloworld.pyx,你可以看到它看起来就像一个普通的 Python Hello World 应用程序,但如前所述,Cython 生成模块。这些模块需要一个名称,以便它们可以被 Python 运行时正确导入。Cython 编译器简单地使用源代码文件的名称。然后它要求我们将这个模块编译成相同的共享对象名称。

总体而言,Cython 源代码文件具有 .pyx.pxd.pxi 扩展名。目前,我们只关心 .pyx 文件;其他文件分别用于 .pyx 模块文件中的 cimportsincludes

下面的截图展示了创建可调用的原生 Python 模块所需的编译流程:

入门 – Hello World

我编写了一个基本的 makefile,这样你只需运行 make 就可以编译这些示例。以下是手动执行此操作的代码:

$ cython helloworld.pyx
$ gcc/clang -g -O2 -fpic `python-config --cflags` -c helloworld.c -o helloworld.o
$ gcc/clang -shared -o helloworld.so helloworld.o `python-config –libs`

使用 Cython 的 distutils

你也可以使用 Python 的 distutilscythonize 来编译这个 HelloWorld 示例模块。打开与 Makefile 并排的 setup.py 文件,你可以看到编译 Cython 模块的另一种方法:

from distutils.core import setup
from Cython.Build import cythonizesetup(ext_modules = cythonize("helloworld.pyx")
)

cythonize 函数作为 ext_modules 部分的一部分,可以将任何指定的 Cython 源代码编译成可安装的 Python 模块。这将 helloworld.pyx 编译成相同的共享库。这为使用 distutils 分发原生模块提供了 Python 实践。

从 Python 调用 C 函数

在谈论 Python 和 Cython 时,我们应该小心清晰,因为它们的语法非常相似。让我们用 C 包装一个简单的 AddFunction 并使其可从 Python 调用。

首先,打开一个名为 AddFunction.c 的文件,并在其中编写一个简单的函数:

#include <stdio.h>int AddFunction(int a, int b) {printf("look we are within your c code!\n");return a + b;
}

这是我们将要调用的 C 代码——只是一个简单的将两个整数相加的函数。现在,让我们让 Python 调用它。打开一个名为 AddFunction.h 的文件,在其中我们将声明我们的原型:

#ifndef __ADDFUNCTION_H__
#define __ADDFUNCTION_H__extern int AddFunction (int, int);#endif //__ADDFUNCTION_H__

我们需要这个原型,以便 Cython 可以看到我们想要调用的函数的原型。在实践中,你已经在自己的项目中有了自己的头文件,其中包含了你的原型和声明。

打开一个名为 AddFunction.pyx 的文件,并在其中插入以下代码:

cdef extern from "AddFunction.h":cdef int AddFunction(int, int)

在这里,我们必须声明我们想要调用的代码。cdef 是一个关键字,表示这是来自将被链接的 C 代码。现在,我们需要一个 Python 入口点:

def Add(a, b):return AddFunction(a, b)

这个 Add 函数是一个位于 PyAddFunction 模块中的 Python 可调用函数,它作为 Python 代码的包装器,以便能够直接调用 C 代码。再次强调,我已经提供了一个方便的 makefile 来生成模块:

$ cd cython-book/chapter1/ownmodule
$ make
cython -2 PyAddFunction.pyx
gcc -g -O2 -fpic -c PyAddFunction.c -o PyAddFunction.o `python-config --includes`
gcc -g -O2 -fpic -c AddFunction.c -o AddFunction.o
gcc -g -O2 -shared -o PyAddFunction.so AddFunction.o PyAddFunction.o `python-config --libs`

注意,AddFunction.c 被编译成相同的 PyAddFunction.so 共享对象。现在,让我们调用这个 AddFunction 并检查 C 是否能正确地添加数字:

$ python
>>> from PyAddFunction import Add
>>> Add(1,2)
look we are within your c code!!
3

注意,AddFunction 内部的打印语句和最终结果打印正确。因此,我们知道控制流到达了 C 代码,并在 C 中进行了计算,而不是在 Python 运行时内部。这是可能的揭示。在某些情况下,人们可能会引用 Python 的速度较慢。使用这种技术使得 Python 代码能够绕过其自己的运行时,并在不受 Python 运行时限制的不安全环境中运行,这要快得多。

Cython 中的类型转换

注意,我们不得不在 Cython 源代码 PyAddFunction.pyx 内部声明一个原型:

cdef extern from "AddFunction.h":cdef int AddFunction(int, int)

这让编译器知道存在一个名为 AddFunction 的函数,它接受两个整数参数并返回一个整数。除了主机和目标操作系统的调用约定之外,编译器需要知道的所有信息都在这里。然后,我们创建了 Python 入口点,它是一个接受两个参数的 Python 可调用函数:

def Add(a, b):return AddFunction(a, b)

在这个入口点内部,它简单地返回了本地的 AddFunction 并将两个 Python 对象作为参数传递。这就是 Cython 那么强大的原因。在这里,Cython 编译器必须检查函数调用并生成代码,以安全地尝试将这些 Python 对象转换为原生 C 整数。当考虑到精度以及潜在的溢出时,这变得很困难,而这恰好是一个主要的使用场景,因为它处理得非常好。此外,请记住,这个函数返回一个整数,Cython 也生成了代码将整数返回值转换为有效的 Python 对象。

小贴士

下载示例代码

您可以从您在 www.PacktPub.com 的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.PacktPub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

摘要

总体来说,我们安装了 Cython 编译器,运行了 Hello World 示例,并考虑到我们需要将所有代码编译成本地共享对象。我们还了解了如何将原生 C 代码封装以使其可从 Python 调用。我们还看到了 Cython 为我们进行的隐式类型转换,以便调用 C 语言。在下一章中,我们将更深入地探讨 Cython 编程,讨论如何使 Python 代码可从 C 调用,以及如何在 Cython 中操作原生 C 数据结构。

第二章。理解 Cython

如我之前提到的,有几种使用 Cython 的方法。由于基础知识对任何 Python 程序员来说都非常熟悉,因此在进入编程语言之前,回顾链接模型非常重要。这是使用 Cython 时驱动应用程序设计的原因。

接下来,我们将更熟悉 Cython 编程语言构造,即 cdefcpdef 之间的区别。然后,我们将探讨如何通过直接与原生 C 类型接口来充分利用 Cython。在本书的后面部分,我们将看到可以使用原生 C++ STL 容器类型。这就是您将获得执行优化之处,因为不需要 Python 运行时来与原生类型一起工作。

最后,我们将看到与 C 和 Python 代码之间的回调操作是多么容易。这是一种有趣的技术,您可以使用它将逻辑从 C 代码卸载到 Python。

因此,在本章中,我们将深入探讨以下主题:

  • 链接模型

  • Cython 关键字 – cdef

  • 类型定义和函数指针

  • 公共关键字

  • 关键字 cpdef

  • 从 C/C++ 到 Python 的日志记录

  • 在 C/C++ 中使用 Python ConfigParser

  • 从 Python 到 C/C++ 的回调

  • Cython PXD

  • 与构建系统的集成

链接模型

在考虑如何扩展或嵌入原生应用程序时,链接模型非常重要。Cython 有两种主要的链接模型:

在 C/C++ 代码中完全嵌入 Python,如下面的截图所示:

链接模型

使用将 Python 运行时嵌入到原生应用程序中的这种方法意味着您可以直接从 C/C++ 代码的任何位置启动代码执行,而不是像在第一章Cython 不会咬人中那样,我们必须运行 Python 解释器并调用导入来执行原生代码。

为了完整性,这里介绍了使用 Cython 的导入模型:

链接模型

这将是 Cython 的更 Pythonic 方法,如果您的代码库主要是 Python,这将非常有帮助。在本书中,我们将在后面回顾 Python lxml 模块的一个示例,它提供了一个 Cython 后端,我们可以将其与原生 Python 后端进行比较,以审查两个代码库执行相同任务的速度和执行情况。

Cython 关键字 – cdef

cdef 关键字告诉编译器此语句是原生 C 类型或原生函数。记得从第一章Cython 不会咬人中我们使用此行来声明 C 原型函数:

cdef int AddFunction(int, int)

这是让我们使用 Python def 关键字将原生 C 函数包装成 Python 可调用对象的行。我们可以在许多上下文中使用它,例如,我们可以声明在函数内部使用的普通变量以加快执行速度:

def square(int x):return x ** 2

这是一个简单的例子,但它会告诉编译器我们总是会平方一个整数。然而,对于正常的 Python 代码来说,这要复杂一些,因为 Python 在处理许多不同类型时必须担心精度丢失的问题。但在这个情况下,我们知道确切的类型以及如何处理它。

你可能也注意到这是一个简单的def函数,但由于它将被传递给 Cython 编译器,所以它将正常工作,并像预期的那样处理类型参数。

结构体

在 Cython 中可以直接处理 C 结构体。例如,这个头文件声明了一个简单的struct

#ifndef __MYCODE_H__
#define __MYCODE_H__struct mystruct {char * string;int integer;char ** string_array;
};extern void printStruct (struct mystruct *);#endif //__MYCODE_H__

这个随机的struct将演示几个概念,包括处理数组。首先,我们必须在 Cython 中声明struct的布局。我们再次可以使用cdef块语法。该块内的所有内容都是cdef,并将包含指定的头文件,这在 Cython 编译器的输出通过 GCC 或 Clang 编译时很重要:

cdef extern from "mycode.h":struct mystruct:char * stringint integerchar ** string_arrayvoid printStruct (mystruct *)

现在我们已经声明了printStruct函数的原型,我们可以使用这个函数来验证 Cython 作用域之外的数据。为了处理这个原始数据类型,我们将创建一个testStruct Python 可调用对象,我们将使用简单的 Python 导入来调用它:

def testStruct ():cdef mystruct scdef char *array [2]s.string = "Hello World"s.integer = 2array [0] = "foo"array [1] = "bar"s.string_array = arrayprintStruct (&s)

让我们更仔细地看看。我们首先在栈上声明了一个struct实例。接下来,我们声明了一个大小为 2 的 C 字符串数组。接下来的几行将通过设置struct的每个成员的值而变得熟悉。但请注意,我们是在栈上声明了字符串数组,然后将字符串数组成员设置为这个实例。这很重要,因为 Cython 将依赖于程序员正确理解内存和栈与堆。但重要的是要注意,在语言之间传递字符串是完全微不足道的。

关于结构体的最后一个注意事项是在定义函数的cdef声明时。如果一个参数是结构体,你永远不要如下声明:

  void myfunc (struct mystruct * x)

相反,我们简单地使用以下方法:

  void myfunc (mystruct * x)

Cython 会自己处理。

testStruct函数有几个细微之处。在 Cython 中,我们有引用操作符"&",它的工作方式与 C 相同。因此,在这个栈上的struct中,我们可以通过引用操作符传递指针,就像在 C 中一样。

注意,Cython 中没有""操作符。当访问struct内部的成员(即使它是一个指针)时,我们简单地使用"."操作符。Cython 理解上下文并将处理它。

从前面的例子以及为了完整性考虑,我们可以简单地实现printStruct函数如下:

#include <stdio.h>
#include "mycode.h"void printStruct (struct mystruct * s)
{printf(".string = %s\n", s->string);printf(".integer = %i\n", s->integer);printf(".string_array = \n");int i;for (i = 0; i < s->integer; ++i)printf ("\t[%i] = %s\n", i, s->string_array [i]);
}

这表明即使我们从 Cython 代码中初始化 C 结构体,它也是一个有效的 C 类型。在下载的代码中简单地运行这个例子如下:

$ cd chapter2/cpy-cdef-reference
$ make
$ python
>>> from mycodepy import testStruct
>>> testStruct ()
.string = Hello World
.integer = 2
.string_array =[0] = foo[1] = bar

这表明 Cython 可以与 C 结构体一起工作——它初始化了 C 结构体并分配了其数据成员,就像它来自 C 一样。

枚举

与 C 枚举的接口很简单。如果你在 C 中有以下的枚举:

enum cardsuit {CLUBS,DIAMONDS,HEARTS,SPADES
};

然后,这可以表达为以下 Cython 声明:

cdef enum cardsuit:CLUBS, DIAMONDS, HEARTS, SPADES

然后,在我们的代码中使用以下作为 cdef 声明:

cdef cardsuit card = CLUBS

这是一个非常小的例子,但重要的是看到它是多么简单。

typedef 和函数指针

在 C/C++ 代码中的 typedef 允许程序员给任何类型赋予一个新名称或别名。例如,可以将 int typedefmyint。或者你可以简单地 typedef 一个结构体,这样你就不必每次都使用 struct 关键字来引用结构体。例如,考虑以下 C structtypedef

struct foobar {int x;char * y;
};
typedef struct foobar foobar_t;

在 Cython 中,这可以描述如下:

cdef struct foobar:int xchar * y
ctypedef foobar foobar_t

注意我们也可以 typedef 指针类型如下:

ctypedef int * int_ptr

我们也可以 typedef C/C++ 函数指针,如下所示:

typedef void (*cfptr) (int)

在 Cython 中,这将如下所示:

ctypedef void (*cfptr)(int)

使用函数指针正如你所期望的那样:

cdef cfptr myfunctionptr = &myfunc

这里有一些关于函数指针的魔法,因为直接从原始 Python 代码调用 Python 函数或反之亦然是不安全的。Cython 理解这种情况,并将为我们包装好以安全地调用。

公共关键字

在 Cython 中,这是一个非常强大的关键字。它允许任何带有 public 修饰符的 cdef 声明输出相应的 C/C++ 头文件,其中相对声明可以从 C/C++ 访问。例如,我们可以声明:

cdef public struct CythonStruct:size_t number_of_elements;char ** elements;

一旦编译器处理了这个,你将得到一个 cython_input.h 的输出:

 struct CythonStruct {size_t number_of_elements;char ** elements;
};

如果你要直接从 C 调用 Python 的 public 声明,主要的一个注意事项是,如果你的链接模型是完全嵌入的并且链接到 libpython.so,你需要使用一些样板代码来正确初始化 Python:

#include <Python.h>int main(int argc, char **argv) {Py_Initialize ();// code in herePy_Finalize ();return 0;
}

在调用该函数之前,如果你有一个 cythonfile.pyx 文件,你需要初始化 Python 模块 example,并按照以下方式编译具有 public 声明的代码:

cdef public void cythonFunction ():print "inside cython function!!!"

你将不仅得到一个 cythonfile.c 文件,还会得到一个 cythonfile.h 文件,它声明了一个名为 extern void initcythonfile (void) 的函数。所以,在调用任何与 Cython 代码相关的内容之前,使用以下代码:

/* Boiler plate init Python */Py_SetProgramName (argv [0]);Py_Initialize ();/* Init our config module into Python memory */initpublicTest ();cythonFunction ();/* cleanup python before exit ... */Py_Finalize ();

在调用函数之前,你可以将调用 initcythonfile 看作在 Python 中的以下操作:

import cythonfile

就像之前的例子一样,这只会影响你如果正在生成一个完全嵌入的 Python 二进制文件。如果你只是编译一个本地模块,你将不需要执行此步骤。

关键字 cpdef

到目前为止,我们已经看到了 Cython 中的两种不同的函数声明,defcdef,用于定义函数。还有一个声明——cpdefdef 是一个仅适用于 Python 的函数,因此它只能从 Python 或 Cython 代码块中调用;从 C 调用不起作用。cdef 是相反的;这意味着它可以从 C 调用,但不能从 Python 调用。例如,如果我们创建一个函数如下:

cpdef public test (int x):…return 1

它将生成以下函数原型:

__PYX_EXTERN_C DL_IMPORT(PyObject) *test(int, int __pyx_skip_dispatch);

public 关键字将确保我们生成所需的头文件,以便我们可以从 C 中调用它。从纯 Python 调用时,我们可以像使用任何其他 Python 函数一样处理它。使用 cpdef 的缺点是原生返回类型是 PyObject *,这要求你确切知道返回类型,并查阅 Python API 文档以访问数据。我更喜欢保持语言之间的绑定更简单,因为这对于 void 函数来说是可行的,并且会更简单。但如果你想要返回数据,可能会很令人沮丧。例如,从前面的代码片段中,如果我们知道我们返回的是 int 类型,我们可以使用以下代码:

long returnValue = PyInt_AsLong (test (1, 0))

注意额外的参数 __pyx_skip_dispatch。由于这是一个实现特定的参数,将其设置为 0,你的调用应该按预期工作,将第一个参数作为指定的参数。我们使用 long 的原因是 Python 中的每个整数都表示为 long。你需要参考 docs.python.org/2/c-api/ 来获取任何其他数据类型,以便从 PyObject 中获取数据。

注意,使用公共的 cpdef Cython 函数并不是一个好主意。是的,这意味着你创建了可以从 C/C++ 和 Python 中调用的函数,而且无需任何更改。但你失去了 Cython 可以提供的类型安全,这对于非常重要。

从 C/C++ 记录到 Python

将一切整合在一起的例子是直接从 C 中重用 Python 日志模块。我们希望有一些宏,例如 infoerrordebug,它们可以处理可变数量的参数,并且像调用简单的 printf 方法一样工作。

为了实现这一点,我们必须为我们的 C/C++ 代码创建一个 Python 日志后端。我们需要一个初始化函数来告诉 Python 关于我们的输出 logfile,以及为每个 infoerrordebug 编写一些包装器。我们可以简单地写出公共的 cdef 包装器如下:

import loggingcdef public void initLoggingWithLogFile(const char * logfile):logging.basicConfig(filename = logfile,level = logging.DEBUG,format = '%(levelname)s %(asctime)s: %(message)s',datefmt = '%m/%d/%Y %I:%M:%S')cdef public void python_info(char * message):logging.info(message)cdef public void python_debug(char * message):logging.debug(message)cdef public void python_error(char * message):logging.error(message)

记住,我们声明我们的公共函数为 cdef;如果它们只是 def,则不能从 C/C++ 中调用。我们可以通过使用 C99 __VA_ARGS__(这允许我们将可变数量的参数传递给函数,因此得名可变参数,这就是 printf 的工作方式)以及一个编译器属性来强制执行参数检查,就像使用 printf 函数族时从错误格式说明符获得的警告和错误一样。现在,我们可以声明并定义我们的 C API 以使用 Python 日志后端:

#ifndef __NATIVE_LOGGING_H__
#define __NATIVE_LOGGING_H__
#define printflike __attribute__ ((format (printf, 3, 4)))extern void printflike native_logging_info(const char *, unsigned, const char *, ...);
extern void printflike native_logging_debug(const char *, unsigned, const char *, ...);
extern void printflike native_logging_error(const char *, unsigned, const char *, ...);#define info(...)  native_logging_info(__FILE__, __LINE__, __VA_ARGS__)
#define error(...) native_logging_debug(__FILE__, __LINE__, __VA_ARGS__)
#define debug(...) native_logging_error(__FILE__, __LINE__, __VA_ARGS__)extern void SetupNativeLogging(const char * logFileName);
extern void CloseNativeLogging();#endif // __NATIVE_LOGGING_H__

现在,我们需要填写这些函数中的每一个,从 SetupNativeLogging 开始:

void SetupNativeLogging(const char * logFileName)
{/* Boiler plate init Python */Py_Initialize();/* Init our config module into Python memory */initPythonLoggingBackend();/* call directly into our cython module  */initLoggingWithLogFile(logFileName);
}

这个函数负责初始化 Python 和 Python 日志后端模块。这相当于 Python 中的 import 语句,但由于我们在 C 中处于主导地位,我们必须原生地加载它。以及相应的 initLoggingWithLogFile,以便记录器将输出到日志文件。我们可以通过使用 va_listvsprintf 函数族将参数列表和格式转换为 C 字符串来打印,从而实现简单的 C infoerrordebug

void native_logging_info(const char * file, unsigned line, const char * fmt, ...)
{char buffer[256];va_list args;va_start(args, fmt);vsprintf(buffer, fmt, args);va_end(args);// append file/line informationchar buf[512];snprintf(buf, sizeof(buf), "%s:%i -> %s", file, line, buffer);// call python logging.infopython_info(buf);
}

现在我们已经将这些宏在 C 中调用它们各自的日志函数,我们只需要定义 CloseNativeLogging,这很简单,因为我们只需要关闭 Python:

void CloseNativeLogging()
{/* cleanup python before exit ... */Py_Finalize();
}

通过将这些功能连接起来,我们就有了一种非常优雅的方式在 C/C++ 中使用 Python,就像它不是什么奇怪的事情:

#include "NativeLogging.h"int main(int argc, char **argv)
{// we want to ensure we use a command line argument for the output log fileif (argc < 2) {return -1;}// use the first argument as log fileSetupNativeLogging(argv[1]);// log out some stuff at different levelsinfo("info message");debug("debug message");error("error message");// close up everything including PythonCloseNativeLogging();return 0;
}

注意,这是 Cython 的完全嵌入链接模型。我决定将所有 Python 特定的代码包裹在实现中。你可以很容易地看到,你甚至可以从使用旧的遗留日志 API 迁移到使用 Python 日志,以便访问大量功能,例如日志记录到网络套接字。

运行这个示例,我们可以看到预期的输出:

$ cd chapter2/PythonLogging
$ make
$ ./example output.log
$ cat output.log
INFO 10/25/2015 07:04:45: main.c:14 -> info message
ERROR 10/25/2015 07:04:45: main.c:15 -> debug message
DEBUG 10/25/2015 07:04:45: main.c-16 -> error message

真正令人高兴的是,我们能够从 C/C++ 代码中保留行信息到 Python 代码。这个示例使用了函数包装概念以及嵌入链接模型。在这个示例中没有使用特殊的编程技巧。

在 C/C++ 中使用 Python ConfigParser

我真的很喜欢 Python 的 ConfigParser API。我发现使用 INI 风格的配置文件比使用 XML 或 JSON 更易于阅读和操作。可用的跨平台库非常少。然而,当你有 Cython 时,你只需要 Python。

对于这个示例,我们将创建一个示例 INI 配置文件,并编写一个简单的 API 来访问部分列表、一个部分中可用的键列表,以及从指定键获取部分值的方法。这三个函数将允许程序员访问任何 INI 文件。

一个示例 INI 文件可以是:

[example]
number = 15
path = some/path/to/something[another_section]
test = something

INI 文件由方括号内的部分组成,后跟键和值。这是一种非常简单的配置方式。Python 的 API 允许根据 ConfigParser 的风味进行变量和替换。首先,我们需要一种查询 INI 文件中部分列表的方法:

from ConfigParser import SafeConfigParser
from libc.stdlib cimport malloccdef public struct ConfigSections:size_t number_of_sectionschar ** sectionscdef public void ParseSectionsFromConfig(const char *config_path, ConfigSections * const sections):parser = SafeConfigParser()with open(config_path) as config_fd:try:parser.readfp(config_fd)sectionsInConfig = parser.sections()sections.number_of_sections = len(sectionsInConfig)sections.sections = <char **>malloc(sections.number_of_sections)for i in range(sections.number_of_sections):sections.sections[i] = sectionsInConfig[i]except:sections.number_of_sections = 0sections.sections = NULL

这里有几个需要注意的地方。首先,以下内容:

cdef public struct ConfigSections

如我们所见,这个 public struct 声明将被输出到相应的头文件中。这意味着我们不需要在 C/C++ 代码中首先定义它:

cdef public void ParseSectionsFromConfig(const char *config_path, ConfigSections * const sections):

这个函数设计用来接收配置文件的路径作为字符串。它还接收指向struct ConfigSections的指针。这个ConfigSections结构允许我们安全地将部分列表返回到 C 代码中。C 是一种非常简单的语言,没有像 C++ STL 库那样的优雅的变量长度结构。

因此,我们必须返回一个指向 C-String 列表的指针以及该列表中的字符串数量。由于这个结构作为参数传递,Cython 代码不需要分配和返回一个指针,这既不高效也不是这种小型结构的标准 C 方法。注意,我们确实需要分配字符串列表:

sections.sections = <char **>malloc(sections.number_of_sections)

与 C++一样,Cython 代码在用 malloc 分配内存时需要显式类型转换。我们将在稍后复习这种类型转换语法,以便进行更高级的使用。接下来,我们需要实现:

cdef public void ParseKeysFromSectionFromConfig(const char * config_path, const char * section, ConfigSectionKeys * keys):

最后,为了从部分内的键获取值:

cdef public char * ParseConfigKeyFromSection(const char *config_path, const char * section, const char * key):

现在我们有了所有这些函数,我们可以编写 C 代码来遍历任何给定配置文件中的部分,并程序化地打印出所有内容,以展示这有多么强大:

#include "PythonConfigParser.h"static
void print_config(const char * config_file)
{struct ConfigSections sections;ParseSectionsFromConfig(config_file, &sections);size_t i;for (i = 0; i < sections.number_of_sections; ++i) {const char *current_section = sections.sections[i];printf("[%s]\n", current_section);struct ConfigSectionKeys sectionKeys;ParseKeysFromSectionFromConfig(config_file, current_section, &sectionKeys);size_t j;for (j = 0; j < sectionKeys.number_of_keys; ++j) {const char * current_key = sectionKeys.keys[j];char *key_value = ParseConfigKeyFromSection(config_file, current_section, current_key);printf("%s = %s\n", current_key, key_value);}free(sectionKeys.keys);}free(sections.sections);
}

使用在栈上传递已分配结构引用的技术,我们消除了大量的内存管理,但由于我们在每个结构内部的数组中分配了内存,我们必须释放它们。但请注意,我们可以简单地返回ParseConfigKeyFromSection的值:

cdef public char * ParseConfigKeyFromSection(const char *config_path, const char * section, const char * key):parser = SafeConfigParser()with open(config_path) as config_fd:try:parser.readfp(config_fd)return parser.get(section, key)except:return NULL

注意

当从 Cython 函数返回 C 字符串时,我们不需要释放任何东西,因为这是由 Python 垃圾回收器管理的。能够从 Cython 返回这样的字符串感觉非常奇怪,但这样做是完全可行的。

运行这个示例,我们可以看到:

$ cd Chapter2/PythonConfigParser
$ make
$ ./example sample.cfg
[example]
number = 15
path = some/path/to/something
[another_section]
test = something

你可以看到,我们成功地将所有部分、键和值从 INI 文件中程序化地解析出来。

从 Python 到 C/C++的回调

回调在异步系统中被广泛使用。例如,libevent 库提供了一个强大的异步核心来处理事件。让我们构建一个示例,将 C 函数作为回调设置到 Python 后端中,这将再次通知 C 代码。首先,我们将声明一个公共回调函数typedef

cdef public:ctypedef void (*callback)(int)

这将输出一个callback类型定义。接下来,我们可以在栈上声明一个全局callback

cdef callback GlobalCallback

一旦设置完成,我们就可以轻松地通知callback。接下来,我们需要一种设置callback的方法,以及一种调用callback的方法:

cdef public void SetCallback(callback cb):global GlobalCallbackGlobalCallback = cb

注意从 Python 传递过来的global关键字,编译器通过这个关键字知道使用global关键字,而不是在那个套件内部创建一个临时实例:

cdef public void Notify(int value):global GlobalCallbackif GlobalCallback != <callback>0:GlobalCallback(value)

Notify将接受一个参数并将这个参数传递给回调。同样,我们需要使用global关键字来确保编译器将使用正确的global关键字。再次使用类型转换,我们确保永远不会调用一个空的callback。接下来,我们需要在 C 代码内部声明一个callback

static
void MyCallback(int val)
{printf("[MYCALLBACK] %i\n", val);
}

然后,我们可以设置callback

SetCallback(&MyCallback);

最后,Notify

Notify(12345);

这是我们应该预期的输出:

$ cd Chapter2/PythonCallbacks
$ make
$ ./example
[MYCALLBACK] 12345

之后,我们将更广泛地使用它来生成一个简单的 Python 消息代理。

Cython PXD

PXD 文件的使用与 C/C++中的头文件非常相似。当编写任何 C/C++代码的绑定时,在.pxd文件中声明所有 C/C++接口是一种良好的做法。这代表Python 外部声明,至少在我的理解中是这样。因此,当我们添加如下块:

cdef extern from "AddFunction.h":cdef int AddFunction(int, int)

我们可以将此直接放入一个bindings.pxd文件,并在任何.pyx文件中的任何时间导入此文件:

cimport bindings

注意cimport用于.pxd文件和简单导入用于所有正常 Python 导入之间的区别。

小贴士

Cython 的输入文件名不能处理文件名中的连字符(-)。最好尝试使用驼峰命名法,因为在 Python 中不能使用cimport my-import

与构建系统的集成

如果您选择共享库方法,此主题基本上取决于您选择的链接模型。我建议使用 Python distutils,如果您正在寻找嵌入式 Python,并且如果您喜欢 GNU 或 autotools,本节提供了一个您可以使用的示例。

Python Distutils

当编译本地 Python 模块时,我们可以在Setup.py构建中使用distutilscythonize。这是 Python 中使用 Cython 作为构建部分的首选方式:

from distutils.core import setup
from Cython.Build import cythonizesetup(ext_modules = cythonize("sourcecode.pyx")
)

此构建文件将支持您使用脚本的任何 Python 版本。当您运行构建时,您的输出将与输入源代码的同一名称相同,在这种情况下是一个共享模块sourcecode.so

GNU/Autotools

要使用 autotools 构建系统在 C/C++应用程序中嵌入 Python 代码,以下代码片段将帮助您。它将使用python-config来获取编译器和链接器标志,以便完成此操作:

found_python=no
AC_ARG_ENABLE(python,AC_HELP_STRING(--enable-python, create python support),found_python=yes
)
AM_CONDITIONAL(IS_PYTHON, test "x%found_python" = xyes)PYLIBS=""
PYINCS=""
if test "x$found_python" = xyes; thenAC_CHECK_PROG(CYTHON_CHECK,cython,yes)if test x"$CYTHON_CHECK" != x"yes" ; thenAC_MSG_ERROR([Please install cython])fiAC_CHECK_PROG(PYTHON_CONF_CHECK,python-config,yes)PYLIBS=`python-config --libs`PYINCS=`python-config --includes`if test "x$PYLIBS" == x; thenAC_MSG_ERROR("python-dev not found")fi
fi
AC_SUBST(PYLIBS)
AC_SUBST(PYINCS)

这将在您的配置脚本中添加--enable-python开关。现在,您有了 Cython 命令found以及PYLIBSPYINCS变量,用于编译所需的编译标志。现在,您需要一个代码片段来了解如何在 automake 中编译源代码中的*.pyx文件:

bin_PROGRAMS = myprog
ACLOCAL_AMFLAGS = -I etc
CFLAGS += -I$(PYINCS)LIBTOOL_DEPS = @LIBTOOL_DEPS@
libtool: $(LIBTOOL_DEPS)$(SHELL) ./config.status libtoolSUFFIXES = .pyx
.pyx.c:@echo "  CPY   " $<@cython -2 -o $@ $<myprog_SOURCES = \src/bla.pyx \
...
myprog_LDADD = \$(PYLIBS)

当您对代码的位置和链接模型感到舒适时,嵌入 Python 变得非常容易。

摘要

本章中有很多使用 Cython 的基本知识。在使用 Cython 时,回顾您想要实现的目标非常重要,因为它的不同使用方式会影响您设计解决方案的方式。我们研究了defcdefcpdef之间的区别。我们创建了公共 C/C++类型和可调用函数的声明。使用这些公共声明,我们展示了 Python 如何回调到 C 代码。对我来说,在本地代码中重用任何 Python 模块非常有用且有趣。我演示了如何从 C 代码中使用 Python 的loggingConfigParser模块。欣赏这些简单的示例,我们将在下一章中看到如何使用 Python 代码扩展 C/C++项目。

第三章:扩展应用程序

如前几章所述,我想向你展示如何使用 Cython 与现有代码交互或扩展。所以,让我们直接开始做吧。Cython 最初是为了使原始 Python 计算更快而设计的。因此,Cython 的初始概念验证是允许程序员使用 Cython 的 cdef 关键字将现有 Python 代码转换为原生类型,以绕过 Python 运行时进行重计算。这一成果是计算时间的性能提升和内存使用降低。甚至可以编写类型安全的包装器来为完全类型化的 Python 代码扩展现有 Python 库。

在本章中,我们将首先看到一个编写 Python 代码的示例。接下来,我将演示 Cython 的 cdef 类,它允许我们将原生 C/C++ 类型包装成垃圾回收的 Python 类。我们还将看到如何通过创建纯 Python 命令对象来扩展原生应用程序 Tmux,该对象直接嵌入到原生代码中。

在本章中,我们将涵盖以下主题:

  • Cython 纯 Python 代码

  • 编译纯 Python 代码

  • Python 垃圾回收器

  • 扩展 Tmux

  • 嵌入 Python

  • Cython 化 struct cmd_entry

  • 实现一个 Tmux 命令

Cython 纯 Python 代码

让我们查看一个实际上来自 Cython 文档的数学应用。我将其等效地用纯 Python 编写,以便我们可以比较速度。如果你打开本章的 primes 示例,你会看到两个程序——Cython 的 primes.pyx 示例和我的纯 Python 版本。它们看起来几乎相同:

def primes(kmax):n = 0k = 0i = 0if kmax > 1000:kmax = 1000p = [0] * kmaxresult = []k = 0n = 2while k < kmax:i = 0while i < k and n % p[i] != 0:i = i + 1if i == k:p[k] = nk = k + 1result.append(n)n = n + 1return result
primes (10000)

这实际上是将 Cython 代码直接转换为 Python。两者都调用 primes (10000),但它们在性能方面的评估时间差异很大:

$ make
cython --embed primes.pyx
gcc -g -O2 -c primes.c -o primes.o `python-config --includes`
gcc -g -O2 -o primes primes.o `python-config –libs`
$ time python pyprimes.py0.18 real         0.17 user         0.01 sys
$ time ./primes0.04 real         0.03 user         0.01 sys

你可以看到,纯 Python 版本在执行相同任务时几乎慢了五倍。此外,几乎每一行代码都是相同的。Cython 可以做到这一点,因为我们已经明确地表达了 C 类型,因此没有类型转换或折叠,我们甚至不需要使用 Python 运行时。我想强调的是,仅通过简单的代码而不调用其他原生库就能获得的速度提升。这就是为什么 Cython 在 SAGE 中如此普遍。

编译纯 Python 代码

Cython 的另一个用途是编译 Python 代码。例如,如果我们回到 primes 示例,我们可以做以下操作:

$ cython pyprimes.py –embed
$ gcc -g -O2 pyprimes.c -o pyprimes `python-config --includes –libs`

然后,我们可以比较同一程序的三个不同版本:使用 cdef 为原生类型编写的 Cython 版本,作为 Python 脚本运行的纯 Python 版本,以及最终,由 Cython 编译的纯 Python 版本,它生成 Python 代码的可执行二进制文件:

  • 首先,使用原生类型的 Cython 版本:

    $ time ./primes
    real    0m0.050s
    user    0m0.035s
    sys     0m0.013s
  • 接下来,可执行的纯 Python 版本:

    $ time ./pyprimes
    real    0m0.139s
    user    0m0.122s
    sys     0m0.013s
  • 最后,Python 脚本版本:

    philips-macbook:primes redbrain$ time python pyprimes.py
    real    0m0.184s
    user    0m0.165s
    sys     0m0.016s

纯 Python 版本运行速度最慢,编译后的 Python 版本运行速度略快,最后,原生类型化的 Cython 版本运行速度最快。我认为这仅仅突出了 Cython 以几种不同的方式为你提供一些动态语言优化的能力。

注意,当将 Python 版本编译成二进制文件时,我在调用 Cython 编译器时指定了 –embed。这告诉编译器为我们嵌入一个主方法,并按预期运行正常的 Python 脚本。

避免使用 Makefile – pyximport

从前面的例子中,你可以看到这是不依赖于任何外部库的代码。要使这样的代码有用,如果我们可以绕过 Makefile 和编译器的调用,那岂不是很好?实际上,在不需要链接其他原生库的情况下,我们可以直接将 .pyx 文件导入到 Python 程序中。然而,你仍然需要将 Cython 作为依赖项安装。

回到第一章,“Cython 不会咬人”,我们可以通过首先导入 pyximport 来简单地导入我们的 helloworld.pyx

>>> import pyximport
>>> pyximport.install()
(None, <pyximport.pyximport.PyxImporter object at 0x102fba4d0>)
>>> import helloworld
Hello World from cython!

在幕后,Cython 会为你调用所有的编译工作,这样你就不必亲自做了。但这引发了一些有趣的想法,比如你只需将 Cython 代码添加到任何 Python 项目中,只要 Cython 是一个依赖项即可。

Python 垃圾回收器

当包装原生 struct 时,例如,可能会非常诱人遵循标准的 C/C++ 习惯用法,并要求 Python 程序员手动调用、分配和释放不同的对象。这非常繁琐,而且不太符合 Python 风格。Cython 允许我们创建 cdef 类,这些类有额外的初始化和析构钩子,我们可以使用这些钩子来控制 struct 的所有内存管理。这些钩子会自动由 Python 垃圾回收器触发,使一切变得简单。考虑以下简单的 struct

typedef struct data {int value;
} data_t;

我们可以将 C struct 的 Cython 声明写入 PyData.pxd,如下所示:

cdef extern from "Data.h":struct data:int valuectypedef data data_t

现在我们已经定义了 struct,我们可以将 struct 包装成一个类:

cimport PyDatacdef class Data(object):cdef PyData.data_t * _nativeData…

将数据包装成这样的类将需要我们在正确的时间分配和释放内存。幸运的是,Cython 几乎暴露了所有的 libc 作为导入:

from libc.stdlib cimport malloc, free

现在我们能够分配和释放内存,剩下要做的只是理解类的生命周期以及在哪里进行钩子。Cython 类提供了两个特殊方法:__cinit____dealloc____cinit__ 提供了一种实例化原生代码的方式,因此在我们的例子中,我们将内存分配给原生 C struct,正如你可以猜到的,在析构时这是垃圾回收器的销毁钩子,并给我们一个机会来释放任何已分配的资源:

def __cinit__(self):self._nativeData = <data_t*>malloc(sizeof(data_t))if not self._nativeData:self._nativeData = NULLraise MemoryError()def __dealloc__(self):if self._nativeData is not NULL:free(self._nativeData)self._nativeData = NULL

需要注意的是,__cinit__不会覆盖 Python 的__init__,更重要的是,__cinit__在此点并不设计为调用任何 Python 代码,因为它不能保证类的完全初始化。一个初始化方法可能如下所示:

def __init__(self, int value):self.SetValue(value)def SetValue(self, int value):self.SetNativeValue(value)cdef SetNativeValue(self, int value):self._nativeData.value = value

注意,我们能够在这类函数上输入参数以确保我们不会尝试将 Python 对象放入struct中,这将会失败。这里令人印象深刻的是,这个类表现得就像是一个普通的 Python 类:

from PyData import Datadef TestPythonData():# Looks and feels like normal python objectsobjectList = [Data(1), Data(2), Data(3)]# Print them outfor dataObject in objectList:print dataObject# Show the MutabilityobjectList[1].SetValue(1234)print objectList[1]

如果你将一个简单的print语句放在__dealloc__钩子上并运行程序,你会看到所有析构函数都按预期执行。这意味着我们刚刚在原生代码上利用了 Python 垃圾回收器。

扩展 Tmux

Tmux是一个受 GNU Screen (tmux.github.io/)启发的终端多路复用器,但它支持更简单、更好的配置。更重要的是,它的实现更干净、更容易维护,并且它还使用了libevent和非常优秀的 C 代码。

我想向你展示如何通过编写 Python 代码而不是 C 代码来扩展 Tmux 的新内置命令。总的来说,这个项目有几个部分,如下所示:

  • 修改 autotool 的构建系统以编译 Cython

  • 为相关声明,如struct cmd_entry创建 PXD 声明

  • 将 Python 嵌入到 Tmux 中

  • 将 Python 命令添加到全局 Tmux cmd_table

让我们快速查看 Tmux 的源代码,特别是包含命令声明和实现的任何cmd-*.c文件。例如,cmd-kill-window.c是命令入口。这告诉 Tmux 命令的名称、别名以及它是否接受参数;最后,它接受一个指向实际命令代码的函数指针:

const struct cmd_entry cmd_kill_window_entry = {"kill-window", "killw","at:", 0, 0,"[-a] " CMD_TARGET_WINDOW_USAGE,0,NULL,NULL,cmd_kill_window_exec
};

因此,如果我们能够实现并初始化包含这些信息的自己的struct,我们就可以运行我们的cdef代码。接下来,我们需要查看 Tmux 如何获取这个命令定义以及它是如何执行的。

如果我们查看tmux.h,我们会找到我们需要操作的所有内容的原型:

extern const struct cmd_entry *cmd_table[];
extern const struct cmd_entry cmd_attach_session_entry;
extern const struct cmd_entry cmd_bind_key_entry;
….

因此,我们需要为我们的cmd_entry定义在这里添加一个原型。接下来,我们需要查看cmd.c;这是命令表初始化的地方,以便稍后可以查找以执行命令:

const struct cmd_entry *cmd_table[] = {&cmd_attach_session_entry,&cmd_bind_key_entry,
…

现在命令表已经初始化,代码在哪里执行呢?如果我们查看tmux.h头文件中的cmd_entry定义,我们可以看到以下内容:

/* Command definition. */
struct cmd_entry {const char  *name;const char  *alias;const char  *args_template;int     args_lower;int     args_upper;const char  *usage;#define CMD_STARTSERVER 0x1
#define CMD_CANTNEST 0x2
#define CMD_SENDENVIRON 0x4
#define CMD_READONLY 0x8int     flags;void     (*key_binding)(struct cmd *, int);int     (*check)(struct args *);enum cmd_retval   (*execc)(struct cmd *, struct cmd_q *);
};

execc钩子是我们真正关心的函数指针,所以如果你grep源代码,你应该会找到以下内容:

Philips-MacBook:tmux-project redbrain$ ack-5.12 execc
tmux-1.8/cmd-queue.c
229:               retval = cmdq->cmd->entry->execc(cmdq->cmd, cmdq);

你可能会注意到在官方的 Tmux Git 中,这个钩子简单地命名为exec。我将其重命名为execc,因为exec是 Python 中的保留字——我们需要避免这种情况。首先,让我们编译一些代码。首先,我们需要让构建系统发挥作用。

Tmux 构建系统

Tmux 使用 autotools,因此我们可以重用第二章,理解 Cython中的片段,以添加 Python 支持。我们可以在configure.ac中添加–enable-python开关,如下所示:

# want python support for pytmux scripting
found_python=no
AC_ARG_ENABLE(python,AC_HELP_STRING(--enable-python, create python support),found_python=yes
)
AM_CONDITIONAL(IS_PYTHON, test "x$found_python" = xyes)PYLIBS=""
PYINCS=""
if test "x$found_python" = xyes; thenAC_CHECK_PROG(CYTHON_CHECK,cython,yes)if test x"$CYTHON_CHECK" != x"yes" ; thenAC_MSG_ERROR([Please install cython])fiAC_CHECK_PROG(PYTHON_CONF_CHECK,python-config,yes)PYLIBS=`python-config --libs`PYINCS=`python-config --includes`if test "x$PYLIBS" == x; thenAC_MSG_ERROR("python-dev not found")fiAC_DEFINE(HAVE_PYTHON)
fi
AC_SUBST(PYLIBS)
AC_SUBST(PYINCS)

这给我们提供了./configure –-enable-python选项。接下来,我们需要查看Makefile.am文件。让我们将我们的 Cython 文件命名为cmdpython.pyx。请注意,Cython 不喜欢文件名中的尴尬字符,如-,如第二章中所述。如果我们想在构建时使 Python 支持条件选项,我们应该将以下内容添加到Makefile.am中:

if IS_PYTHON
PYTHON_SOURCES = cmdpython.pyx
else
PYTHON_SOURCES =
endif# List of sources.
dist_tmux_SOURCES = \$(PYTHON_SOURCES) \
...

我们必须确保它首先被需要并编译。记住,如果我们创建public声明,Cython 会为我们生成一个头文件。我们只需将我们的公共头文件添加到tmux.h中,以保持头文件非常简单。然后,为了确保 Cython 文件在构建时被 automake 识别并正确编译,根据正确的依赖管理,我们需要添加以下内容:

SUFFIXES = .pyx
.pyx.c:@echo "  CPY   " $<@cython -2 -o $@ $<

这添加了后缀规则,以确保*.pyx文件被 Cython 化,然后像任何正常的 C 文件一样编译生成的.c文件。如果你在 autotools 项目中恰好使用了AM_SILENT_RULES([yes]),这个片段运行得很好,因为它正确地格式化了 echo 消息。最后,我们需要确保在配置脚本中的AC_SUBST中添加必要的CFLAGSLIBS选项到编译器:

CFLAGS += $(PYINCS)
tmux_LDADD = \$(PYLIBS)

现在你的构建系统应该已经准备好了,但由于所做的更改,我们必须现在重新生成 autotools 的内容。只需运行./autogen.sh

嵌入 Python

现在我们有文件正在编译,我们需要初始化 Python。我们的模块。Tmux 是一个分叉的服务器,客户端连接到,所以尽量不要把它看作一个单线程系统。它是一个客户端服务器,所以所有命令都在服务器上执行。现在,让我们找到服务器中事件循环开始的地方,并在服务器这里初始化和最终化,以确保正确完成。查看int server_start(int lockfd, char *lockfile),我们可以添加以下内容:

#ifdef HAVE_PYTHONPy_InitializeEx (0);
#endifserver_loop();
#ifdef HAVE_PYTHONPy_Finalize ();
#endif

Python 现在被嵌入到 Tmux 服务器中。注意,我使用的是Py_InitializeEx (0)而不是简单地使用Py_Initialize。这复制了相同的行为,但不会启动正常的 Python 信号处理器。Tmux 有自己的信号处理器,所以我不想覆盖它们。当扩展像这样的现有应用程序时,使用Py_InitializeEx (0)可能是一个好主意,因为它们通常实现自己的信号处理。使用这个选项可以阻止 Python 尝试处理会冲突的信号。

Cython 化 struct cmd_entry

接下来,让我们考虑创建一个cythonfile.pxd文件,用于 Tmux 必要的cdef声明,我们需要了解的。我们需要查看struct cmd_entry声明,并从这个声明反向工作:

struct cmd_entry {const char  *name;const char  *alias;const char  *args_template;int     args_lower;int     args_upper;const char  *usage;int     flags;void     (*key_binding)(struct cmd *, int);int     (*check)(struct args *);enum cmd_retval   (*execc)(struct cmd *, struct cmd_q *);
};

如您所见,cmd_entry依赖于几个其他类型,因此我们需要稍微回溯一下。如果您想偷懒并冒险,如果您不关心通过类型转换任何指针(如void *)来正确访问数据,有时您可以侥幸逃脱。但如果你是一个经验丰富的 C 程序员,你知道这是相当危险的,应该避免。您可以看到这个类型依赖于struct cmd *struct cmd_q *struct args *。我们理想情况下想在某个时刻访问这些,所以逐个实现它们是个好主意,因为其余的都是原生 C 类型,Cython 可以理解。

实现enum应该是迄今为止最简单的:

/* Command return values. */
enum cmd_retval {CMD_RETURN_ERROR = -1,CMD_RETURN_NORMAL = 0,CMD_RETURN_WAIT,CMD_RETURN_STOP
};

然后,将其转换为以下形式:

cdef enum cmd_retval:CMD_RETURN_ERROR = -1CMD_RETURN_NORMAL = 0CMD_RETURN_WAIT = 1CMD_RETURN_STOP = 2

现在我们有了exec钩子的返回值,接下来我们需要查看struct cmd并实现它:

struct cmd {const struct cmd_entry  *entry;struct args    *args;char      *file;u_int       line;TAILQ_ENTRY(cmd)   qentry;
};

看一下TAILQ_ENTRY。这是一个简单的预处理宏,是BSD libc的扩展,可以将任何类型转换为它自己的链表。我们可以忽略它:

 cdef struct cmd:cmd_entry * entryargs * aargschar * fileint line

注意,这个struct依赖于struct cmd_entrystruct args的定义,我们还没有实现。现在不用担心这个问题;暂时先放它们在这里。接下来,让我们实现struct args,因为它很简单:

/* Parsed arguments. */
struct args {bitstr_t  *flags;char    *values[SCHAR_MAX];int     argc;char         **argv;
};

注意,它使用了bitstr_t和可变长度的数组列表。我选择忽略bitstr_t,因为它是一个系统依赖的头文件,实现起来相当棘手。让我们简单地将它们转换为char *char **以使事情运行起来:

 cdef struct args:char * flagschar **valuesint argcchar **argv

现在已经将args结构 Cython 化,让我们实现struct cmd_q,这稍微有点棘手:

/* Command queue. */
struct cmd_q {int       references;int       dead;struct client    *client;int       client_exit;struct cmd_q_items   queue;struct cmd_q_item  *item;struct cmd    *cmd;time_t       time;u_int       number;void       (*emptyfn)(struct cmd_q *);void      *data;struct msg_command_data  *msgdata;TAILQ_ENTRY(cmd_q)       waitentry;
};

还有许多其他结构依赖于它,但在这里我们不会看到它们。让我们现在尝试将这些转换为void *;例如,struct client *。我们可以将其转换为void *,然后struct cmd_q_items简单地转换为int,即使这不正确。只要我们不打算尝试访问这些字段,我们就会没事。但请记住,如果我们使用 Cython 的sizeof,我们可能会遇到由 C 和 Cython 分配的不同大小的内存损坏。我们可以继续处理其他类型,如struct cmd_q_item *,并将它们再次转换为void *。最后,我们来到time_t,我们可以重用 Cython 的libc.stdlib cimport time。这是一个很好的练习,为 C 应用程序实现 Cython 声明;它真正锻炼了你的代码分析能力。在处理非常长的结构时,请记住,我们可以通过将它们转换为void来使事情运转起来。如果你关心你的 Cython API 中的数据类型,请小心处理struct的对齐和类型:

 cdef struct cmd_q:int referencesint deadvoid * clientint client_exitint queuevoid * itemcmd * cmdint timeint numbervoid (*emptyfn)(cmd_q *)void * msgdata

这是对许多项目特定内部结构的深入探讨,但我希望您能理解——我们实际上并没有做什么特别可怕的事情。我们甚至作弊了,将我们实际上并不关心的东西进行了类型转换。在实现了所有这些辅助类型之后,我们最终可以实施我们关心的类型,即struct cmd_entry

cdef struct cmd_entry:char * namechar * aliaschar * args_templateint args_lowerint args_upperchar * usageint flagsvoid (*keybinding)(cmd *, int)int (*check)(args *)cmd_retval (*execc)(cmd *, cmd_q *)

通过这个cmdpython.pxd文件,我们现在可以实现我们的 Tmux 命令!

实现 Tmux 命令

Cython 有一个注意事项是我们不能像在 C 中那样静态初始化结构体,因此我们需要创建一个钩子,以便在 Python 启动时初始化cmd_entry

cimport cmdpythoncdef public cmd_entry cmd_entry_python

通过这种方式,我们现在有了cmd_entry_python的公共声明,我们将在启动钩子中初始化它,如下所示:

cdef public void tmux_init_cython () with gil:cmd_entry_python.name = "python"cmd_entry_python.alias = "py"cmd_entry_python.args_template = ""cmd_entry_python.args_lower = 0cmd_entry_python.args_upper = 0cmd_entry_python.usage = "python usage..."cmd_entry_python.flags = 0#cmd_entry_python.key_binding = NULL#cmd_entry_python.check = NULLcmd_entry_python.execc = python_exec

记住,因为我们是在顶层声明的,所以我们知道它位于堆上,不需要向结构体声明任何内存,这对我们来说非常方便。你之前已经见过结构体的访问方式;函数套件应该看起来很熟悉。但让我在这里强调几点:

  • 我们声明public是为了确保我们可以调用它。

  • 执行钩子只是一个cdef Cython 函数。

  • 最后,你可能注意到了gil。我将在第五章 高级 Cython 中解释这个用于什么。

现在,让我们看看一个简单的执行钩子:

cdef cmd_retval python_exec (cmd * cmd, cmd_q * cmdq) with gil:cdef char * message = "Inside your python command inside tmux!!!"log_debug (message)return CMD_RETURN_NORMAL;

现在将这个钩子连接到 Tmux 没有太多剩余的工作要做。只需将其添加到cmd_table,并将启动钩子添加到服务器初始化中。

注意

注意,我在log_debug函数中向 PXD 添加了一些内容;如果你查看 Tmux,这是一个VA_ARGS函数。Cython 目前还不理解这些,但我们可以通过将其转换为接受字符串的函数来简单地“黑客”它,让它运行起来。只要我们不尝试像使用任何printf一样使用它,我们应该就没事了。

将一切连接起来

现在,我们还需要对 Tmux 进行一点小小的调整,但这并不痛苦,一旦完成,我们就可以自由地发挥创意。从根本上说,我们应该在忘记之前在server.c中调用cmd_entry初始化钩子:

#ifdef HAVE_PYTHONPy_InitializeEx (0);tmux_init_cython ();
#endifserver_loop();#ifdef HAVE_PYTHONPy_Finalize ();
#endif

现在已经完成,我们需要确保将cmd_entry_python外部声明添加到tmux.h中:

extern const struct cmd_entry cmd_wait_for_entry;
#ifdef HAVE_PYTHON
# include "cmdpython.h"
#endif

最后,将其添加到cmd_table

const struct cmd_entry *cmd_table[] = {&cmd_attach_session_entry,&cmd_bind_key_entry,&cmd_break_pane_entry,
…&cmd_wait_for_entry,&cmd_entry_python,NULL
};

现在已经完成,我认为我们可以开始了——让我们测试一下这个小家伙。用以下方式编译 Tmux:

$ ./configure –enable-python
$ make
$ ./tmux -vvv
$ tmux: C-b :python
$ tmux: exit

我们可以查看tmux-server-*.log来查看我们的调试信息:

complete key ^M 0xd
cmdq 0xbb38f0: python (client 8)
Inside your python command inside tmux!!!
keys are 1 (e)

我希望你现在可以看到,你可以很容易地将其扩展到做你自己的选择,比如使用 Python 库直接调用你的音乐播放器,并且所有这些都将与 Tmux 集成在一起。

概述

本章展示了许多不同的技术和想法,但它应该作为常见技术的强大参考。我们看到了使用本地类型绕过运行时的加速,并将编译的 Python 代码编译成自己的二进制文件。pyximport语句显示我们可以绕过编译,简单地导入.pyx文件,就像它是普通的 Python 文件一样。最后,我在本章的结尾通过逐步演示我的过程,展示了如何将 Python 嵌入到 Tmux 中。在下一章中,我们将看到使用gdb进行调试的实际操作,以及使用 Cython 的一些注意事项。

第四章:调试 Cython

由于 Cython 程序编译成原生代码,我们无法使用 Python 调试器逐步执行代码。然而,我们可以使用 GDB。GNU 项目调试器GDB)是一个跨平台调试器。Python 插件支持从版本 7.0 开始添加,这被用来将 Cython 支持添加到gdb中作为一个简单的脚本;这意味着你可以无缝地在 C/C++代码和 Cython 之间逐步执行。

当涉及到语言绑定时,保持接口尽可能简单是一个好的做法。这将使调试变得简单,直到你对资源管理或稳定性方面的绑定满意。我将迭代一些 GDB 和注意事项的例子。

在本章中,我们将涵盖以下主题:

  • 使用 GFB 与 Cython

  • Cython 注意事项

使用 GDB 与 Cython

要调试 Cython,你需要 GDB >= 7.0。在 Mac OS X Xcode 中,构建工具已移动到 LLVM 和 lldb 作为相应的调试器。你可以使用 homebrew 安装gdb

$ brew install gdb

由于 Cython 代码编译成 C/C++,我们无法使用 Python 调试器。因此,在没有 Cython 插件的情况下调试时,你将逐步执行生成的 C/C++代码,这不会很有帮助,因为它不会理解 Cython 程序的环境。

运行 cygdb

Cygdb 作为 Cython 的一部分安装,并且是 GDB 的包装器(它通过传递参数调用 GDB 以设置 Cython 插件)。在您能够调试 Cython 代码之前,我们需要生成调试信息。就像 C/C++一样,我们需要指定编译器选项以生成可调试的代码,我们可以在调用 Cython 编译器时传递–gdb

$ cython --gdb cycode.pyx

注意

在您开始在 Debian 上调试之前,您需要安装 Python 调试信息包和 GDB,因为它们不是与build-essential一起安装的。要安装这些,请运行以下命令:

$ sudo apt-get install gdb build-essential cython python-dbg

现在你已经安装了 GDB 和生成的调试信息,你可以使用以下命令启动 Cython 调试器:

$ cygdb . --args python-dbg main.py

一旦你熟悉了 GDB,你就可以简单地使用所有的正常gdb命令。然而,cygdb的全部目的在于我们可以使用 Cython 命令,我们将在下面使用并解释:

(gdb) cy break
__init__                cycode.foobar.__init__  cycode.foobar.print_me  cycode.func             func                    print_me

如果你使用 Tab 键自动完成cy break,你会看到一个可以设置 Cython 断点的符号列表。接下来,我们需要运行程序并继续到我们的断点,如下所示:

(gdb) cy break func

Function "__pyx_pw_6cycode_1func" not defined.
Breakpoint 1 (__pyx_pw_6cycode_1func) pending.

现在我们已经设置了断点,我们需要运行程序:

(gdb) cy run
1    def func (int x):

现在我们已经到达了func函数的声明,我们可以继续并做一些内省,如下所示:

(gdb) cy globals
Python globals:__builtins__ = <module at remote 0x7ffff7fabb08>__doc__      = None__file__     = '$HOME/chapter4/gdb1/cycode.so'__name__     = 'cycode'__package__  = None__test__     = {}foobar       = <classobj at remote 0x7ffff7ee50b8>func         = <built-in function func>
C globals:

globals命令将显示当前帧作用域中的任何全局标识符,因此我们可以看到func函数和classobj foobar。我们可以通过列出代码和逐步执行代码来进一步检查:

(gdb) cy list1    def func (int x):2        print x3        return x + 14

我们也可以按照以下方式逐步执行代码:

(gdb) cy step
1
4    cycode.func (1)(gdb) cy list1    #!/usr/bin/python2    import cycode34    cycode.func (1)
>    5    object = cycode.foobar ()6    object.print_me ()(gdb) cy step
3        return x + 1(gdb) cy list1    def func (int x):2        print x
>    3        return x + 145    class foobar:6        x = 07        def __init__ (self):

你甚至可以从类中获得相当整洁的列表:

(gdb) cy list3        return x + 145    class foobar:6        x = 07        def __init__ (self):
>    8            self.x = 1910        def print_me (self):11            print self.x

我们甚至可以看到当前 Python 状态的后退跟踪:

(gdb) cy bt
#9  0x000000000047b6a0 in <module>() at main.py:66    object.print_me ()
#13 0x00007ffff6a05ea0 in print_me() at /home/redbrain/cython-book/chapter4/gdb1/cycode.pyx:88            self.x = 1

帮助信息可以通过运行以下命令找到:

(gdb) help cy

我想你已经明白了!尝试一下,查看帮助文档,并亲自尝试这些,以获得使用 cygdb 进行调试的感觉。为了获得良好的感觉,你真的需要通过 GDB 练习并熟悉它。

Cython 注意事项

当混合 C 和 Python 代码时,Cython 有一些需要注意的注意事项。在构建生产就绪的产品时,参考这些注意事项是个好主意。

类型检查

你可能已经注意到,在前面的代码示例中,我们能够使用 mallocvoid * 指针转换为我们的扩展类型,使用 malloc。Cython 支持一些更高级的类型检查,如下所示:

char * buf = <char *> malloc (sizeof (...))

在基本类型转换中,Cython 支持 <type?> 用于类型检查:

char * buf  = <char *?> malloc (...)

这将进行类型检查,如果被转换的类型不是 char * 的子类,则会抛出错误。所以,在这种情况下,它会通过;然而,如果你要这样做:

cdef class A:pass
cdef class B (A):passdef myfunc ():cdef A class1 = A ()cdef B class2 = B ()cdef B x = <B?> class1

这将在运行时返回一个错误:

Traceback (most recent call last):File "main.py", line 2, in <module>myfunc ()File "cycode.pyx", line 12, in cycode.myfunc (cycode.c:714)cdef B x = <B?> class1
TypeError: Cannot convert cycode.A to cycode.B

因此,这可以为你的 Cython API 增加更多的类型安全性。

取引用运算符 (*)

在 Cython 中,我们没有取引用运算符。例如,如果你要将 C 数组和长度传递给一个函数,你可以使用指针算术来迭代和访问数组的元素:

  int * ptr = array;int i;for (i = 0; i < len; ++i)printf ("%i\n", *ptr++);

在 Cython 中,我们必须通过访问元素零来稍微明确一些。然后,我们增加指针:

    cdef int icdef int * ptr = arrayfor i in range (len):print ptr [0]ptr = ptr + 1

这里并没有什么特别之处。如果你想取消引用 int *x,你只需使用 x[0]

Python 异常

另一个需要关注的话题是,如果你的 Cython 代码将异常传播到 C 代码中会发生什么。在下一章中,我们将介绍 C++ 原生异常如何与 Python 交互,但在 C 中我们没有这个。考虑以下代码:

cdef public void myfunc ():raise Exception ("Raising an exception!")

这只是将异常返回到 C,并给出以下内容:

$ ./test
Exception: Exception('Raising an exception!',) in 'cycode.myfunc' ignored
Away doing something else now...

如你所见,打印了一个警告,但没有发生异常处理,所以程序继续执行其他操作。这是因为不返回 Python 对象的简单 cdef 函数没有处理异常的方法;因此,打印了一个简单的警告消息。如果我们想控制 C 程序的行为,我们需要在 Cython 函数原型中声明异常。

有三种形式可以做到这一点。首先,我们可以这样做:

cdef int myfunc () except -1:cdef int retval = -1….return retval

这使得函数在返回 -1 时抛出异常。这也导致异常被传播到调用者;因此,在 Cython 中,我们可以这样做:

cdef public void run ():try:myfunc ()somethingElse ()except Exception:print "Something wrong"

你还可以使用 maybe 异常(正如我希望称呼它),其形式如下:

cdef int myfunc () except ? -1:cdef int retval = -1….return retval

这意味着它可能是一个错误,也可能不是。Cython 从 C API 生成对 PyErr_Occurred 的调用以执行验证。最后,我们可以使用通配符:

cdef int myfunc () except *:

这使得它总是调用 PyErr_Occurred,你可以通过 PyErr_PrintEx 或其他方式在 docs.python.org/2/c-api/exceptions.html 检查。

注意,函数指针声明也可以在它们的原型中处理这个问题。只需确保返回类型与异常类型匹配,该类型必须是枚举、浮点、指针类型或常量表达式;如果不是这种情况,你将得到一个令人困惑的编译错误。

C/C++ 迭代器

Cython 对 C 风格的 for 循环有更多支持,并且它还可以根据迭代器的声明方式对 range 函数进行进一步的优化。通常,在 Python 中,你只需做以下操作:

for i in iterable_type: …

这在 PyObjects 上是可行的,因为它们理解迭代器,但 C 类型没有这些抽象。你需要对你的数组类型进行指针运算以访问索引。例如,我们首先可以使用 range 函数做以下操作:

cdef void myfunc (int length, int * array)cdef int ifor i in range (length):print array [i]

当在 C 类型上使用范围函数时,例如以下使用 cdef int i 的示例,它针对实际的 C 数组访问进行了优化。我们还可以使用其他几种形式。我们可以将循环转换为以下形式:

cdef int i
for i in array [:length]: print i

这看起来更像是一个正常的 Python for 循环执行迭代,分配 i,索引数据。还有一个 Cython 引入的最后一个形式,使用 for .. from 语法。这看起来像真正的 C for 循环,我们现在可以写:

def myfunc (int length, int * array):cdef int ifor i from 0 <= i < length;print array [i]

我们还可以引入步长:

for i from 0 <= i < length by 2:print array [i]

这些额外的 for 循环结构在处理大量 C 类型时特别有用,因为它们不理解额外的 Python 结构。

布尔错误

当你尝试在 Cython 中使用 bool 时,你会得到以下结果:

cycode.pyx:2:9: 'bool' is not a type identifier

因此,你需要使用这个:

from libcpp cimport bool

当你编译它时,你会得到以下结果:

cycode.c: In function '__pyx_pf_6cycode_run':
cycode.c:642: error: 'bool' undeclared (first use in this function)
cycode.c:642: error: (Each undeclared identifier is reported only once
cycode.c:642: error: for each function it appears in.)
cycode.c:642: error: expected ';' before '__pyx_v_mybool'
cycode.c:657: error: '__pyx_v_mybool' undeclared (first use in this function)

你需要确保你使用的是 C++ 编译器进行编译,因为 bool 是一个原生类型。

Const 关键字

Cython 在 0.18 之前不理解 const 关键字,但我们可以通过以下 typedefs 来解决这个问题:

cdef extern from *:ctypedef char* const_char_ptr "const char*"

现在,我们可以像以下这样使用 const 关键字:

cdef public void foo_c(const_char_ptr s):...

如果你使用的是 Cython 0.18 或更高版本,你可以像从 C 那样使用 const

多个 Cython 输入

Cython 不处理多个 .pyx 文件。因此,Cython 有另一个关键字和约定——.pxi。这是一个额外的包含文件,它就像 C 包含文件一样工作。所有其他 Cython 文件都会被拉入一个文件,以创建一个 Cython 编译。为此,你需要做以下操作:

include "myothercythonfile.pxi"

重要的是要记住,这作为一个 C 包含文件工作,并将从文件中包含代码到包含点的代码放入。

结构体初始化

当声明 struct 时,你不能像以下这样进行正常的 C 初始化:

struct myStruct {int x;char * y;
}
struct myStruct x = { 2, "bla" };

你需要做以下操作:

cdef myStruct x:
x.x = 2
x.y = "bla"

因此,你手动更详细地指定字段。所以,当使用结构体时,你应该确保在使用之前使用 memset 或显式设置每个元素。

调用纯 Python 模块

你总是可以调用一些纯 Python 代码(非 Cython),但你应该始终保持警惕,并使用 Python disutils 确保模块在开发环境之外正确安装。

摘要

总体来说,我们已经看到了使用 cygdb 包装器进行的一些基本调试。更重要的是,我们检查了一些 Cython 的注意事项和特性。在下一章中,我们将看到如何从 Cython 直接绑定 C++代码以及如何与 C++构造工作,例如模板和 STL 库,特别是。我们还将看到 GIL 如何影响在 Cython 和 C/C++中与代码的工作。

第五章。高级 Cython

在整本书中,我们一直是在混合 C 和 Python。在本章中,我们将深入研究 C++ 和 Cython。随着 Cython C++ 的每一次发布,支持都得到了改善。这并不是说它现在还不能使用。在本章中,我们将涵盖以下主题:

  • 使本地的 C++ 类可从 Python 调用。

  • 包装 C++ 命名空间和模板

  • 异常如何从 C++ 和 Python 中传播

  • C++ 的 new 和 del 关键字

  • 运算符重载

  • Cython gil 和 nogil 关键字

我们将通过将一个网络服务器嵌入到一个玩具 C++ 消息服务器中来结束本章。

Cython 和 C++

在所有绑定生成器中,Cython 与 C++ 的结合最为无缝。C++ 在编写其绑定时有一些复杂性,例如调用约定、模板和类。我发现这种异常处理是 Cython 的一个亮点,我们将查看每个示例。

命名空间

我首先介绍命名空间,因为 Cython 使用命名空间作为在模块中引用 C++ 代码的方式。考虑以下具有以下命名空间的 C++ 头文件:

#ifndef __MY_HEADER_H__
#define __MY_HEADER_H__namespace mynamespace {
….
}#endif //__MY_HEADER_H__

你可以用 cdef extern 声明将其包装起来:

cdef extern from "header.h" namespace "mynamespace":…

你现在可以在 Cython 中像通常对模块那样访问它:

import cythonfile
cythonfile.mynamespace.attribute

只需使用命名空间,它就真的感觉像是一个 Python 模块。

我猜测,你大部分的 C++ 代码都是围绕使用类来编写的。作为一个面向对象的语言,Cython 可以无缝地处理这一点:

#ifndef __MY_HEADER_H__
#define __MY_HEADER_H__namespace mynamespace {void myFunc (void);class myClass {public:int x;void printMe (void);};
}#endif //__MY_HEADER_H__

我们可以使用 Cython 的 cppclass 关键字。这个特殊的关键字允许你声明 C++ 类并直接与之交互,因此你不需要编写包装代码,这在大型项目中可能会非常繁琐且容易出错。使用之前的命名空间示例,我们将包装命名空间,然后是命名空间内的类:

cdef extern from "myheader.h" namespace "mynamespace":void myFunc ()cppclass myClass:int xvoid printMe ()

这就像 C 类型一样简单。尽管现在,你有一个本地的 C++ 对象,这可以非常强大。

记住,Cython 只会关心 public 属性。由于封装了私有和受保护的方法,这些是调用者可以访问的唯一属性。不可能扩展 C++ 类。现在,你可以像处理 cdef 结构体一样处理这些。只需像以前一样使用 . 运算符来访问所有必要的属性。

C++ 的 new 和 del 关键字

Cython 理解 C++ 的 new 关键字;所以,考虑你有一个 C++ 类:

 class Car {int doors;int wheels;public:Car ();~Car ();void printCar (void);void setWheels (int x) { wheels = x; };void setDoors (int x) { doors = x; };};

它在 Cython 中如下定义:

cdef extern from "cppcode.h" namespace "mynamespace":cppclass Car:Car ()void printCar ()void setWheels (int)void setDoors (int)

注意,我们没有声明 ~Car 析构函数,因为我们从未直接调用它。它不是一个显式可调用的公共成员;这就是为什么我们从未直接调用它,但 delete 会,编译器将确保在它将离开栈作用域时调用它。要在 Cython 代码中在堆上实例化原始 C++ 类,我们可以简单地运行以下代码:

cdef Car * c = new Car ()

然后,你可以使用 Python 的 del 关键字在任何时候使用 del 删除对象:

del c

你会发现析构函数的调用正如你所预期的那样:

$ cd chapter5/cppalloc; make; ./test
Car constructor
Car has 3 doors and 4 wheels
Car destructor

我们也可以声明一个栈分配的对象,但它必须只有一个默认构造函数,如下所示:

cdef Car c

在 Cython 中,使用此语法无法传递参数。但是,请注意,你不能在这个实例上使用del,否则你会得到以下错误:

cpycode.pyx:13:6: Deletion of non-heap C++ object

异常

使用 C++异常处理,你可以感受到 Cython 在 C++代码中的无缝感。如果抛出了任何异常,例如内存分配,Cython 将处理这些异常并将它们转换为更有用的错误,你仍然会得到有效的 C++异常对象。Python 也会理解这些是否被捕获以及是否按需处理。此表为你提供了 Python 异常在 C++中映射的概览:

C++ Python
bad_alloc MemoryError
bad_cast TypeError
domain_error ValueError
invalid_argument ValueError
ios_base::failure IOError
out_of_range IndexError
overflow_error OverflowError
range_error ArithmeticError
underflow_error ArithmeticError
所有其他异常 RuntimeError

例如,考虑以下 C++代码。当调用myFunc函数时,它将简单地抛出一个异常。首先,我们使用以下内容定义一个异常:

namespace mynamespace {class mycppexcept: public std::exception {virtual const char * what () const throw () {return "C++ exception happened";}};void myFunc (void) throw (mycppexcept);
}

现在,我们编写一个抛出异常的函数:

void mynamespace::myFunc (void) throw (mynamespace::mycppexcept) {mynamespace::mycppexcept ex;cout << "About to throw an exception!" << endl;throw ex;
}

我们可以在 Cython 中使用以下方式调用它:

cdef extern from "myheader.h" namespace "mynamespace":void myFunc () except +RuntimeError

当我们运行函数时,我们得到以下输出:

>>> import cpycode
About to throw an exception!
Traceback (most recent call last):File "<stdin>", line 1, in <module>File "cpycode.pyx", line 3, in init cpycode (cpycode.cpp:763)myFunc ()
RuntimeError: C++ exception happened
>>> ^D

如果你想在 Python 代码中捕获 C++异常,你可以像平常一样使用它:

try:
...
except RuntimeError:
...

注意,我们告诉 Cython 将任何异常转换为RuntimeError。这很重要,以确保你理解哪些接口和位置可能会抛出异常。未处理的异常看起来真的很糟糕,并且可能更难调试。在这个阶段,Cython 无法对状态做出太多假设,因为编译器在 C++代码级别上不会对可能未处理的异常抛出错误。如果发生这种情况,你将得到以下内容,因为没有准备好异常处理程序:

$ cd chapter5/cppexceptions; make; python
Python 2.7.2 (default, Oct 11 2012, 20:14:37)
[GCC 4.2.1 Compatible Apple Clang 4.0 (tags/Apple/clang-418.0.60)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import cpycode
About to throw an exception!
Segmentation fault: 11

布尔类型

如前一章所示,要使用 C++的本地bool类型,你首先需要导入以下内容:

from libcpp cimport bool

然后,你可以使用bool作为正常的cdef。如果你想使用纯 PyObject bool类型,你需要导入以下内容:

from cpython cimport bool

你可以使用正常的truefalse值来分配它们。

重载

由于 Python 支持重载以包装 C++重载,只需按正常方式列出成员即可:

cdef foobar (int)
cdef foobar (int, int)
…

Cython 理解我们处于 C++模式,并且可以像正常一样处理所有类型转换。有趣的是,它还可以轻松处理运算符重载,因为它只是另一个钩子!例如,让我们再次以Car类为例,执行一些如下的运算符重载操作:

namespace mynamespace {class Car {int doors;int wheels;public:Car ();~Car ();Car * operator+(Car *);void printCar (void);void setWheels (int x) { wheels = x; };void setDoors (int x) { doors = x; };};
};

记得将这些运算符重载类成员添加到你的 Cython 化类中;否则,你的 Cython 将抛出以下错误:

Invalid operand types for '+' (Car *; Car *)

运算符重载的 Cython 声明看起来正如你所期望的那样:

cdef extern from "cppcode.h" namespace "mynamespace":cppclass Car:Car ()Car * operator+ (Car *)void printCar ()void setWheels (int)void setDoors (int)

现在,你可以执行以下操作:

cdef Car * ccc = c[0] + cc
ccc.printCar ()

这将在命令行上给我们以下输出:

$ cd chapter5/cppoverloading; make; ./test
Car constructor
Car constructor
Car has 3 doors and 4 wheels
Car has 6 doors and 8 wheels
inside operator +
Car constructor
Car has 9 doors and 12 wheels

所有的处理方式都如你所预期。对我来说,这证明了 Guido 设计 Python 类所受到的启发原则。

模板

Cython 支持模板。尽管如此,为了完整性,模板元编程模式可能无法正确包装或无法编译。随着每个版本的发布,这都在不断改进,所以请带着一点点盐来接受这个评论。

C++ 类模板工作得非常好;我们可以实现一个名为 LinkedList 的模板,如下所示:

cppclass LinkedList[T]:LinkedList ()void append (T)int getLength ()
...

现在,你可以使用名为 T 的声明来访问模板类型。你可以继续阅读 chapter5/cpptemplates 中的其余代码。

静态类成员属性

有时,在类中,拥有一个静态属性,如以下内容,是有用的:

namespace mynamespace {class myClass {public:static void myStaticMethod (void);};
}

在 Cython 中,没有通过 static 关键字支持这一点,但你可以将这个函数绑定到一个命名空间上,使其成为以下内容:

cdef extern from "header.h" namespace "mynamespace::myClass":void myStaticMethod ()

现在,你只需在 Cython 中将其作为全局方法调用即可。

调用 C++ 函数 - 注意事项

当你编写代码从 C 调用 C++ 函数时,你需要将原型包装在以下内容中:

extern "C" { … }

这允许你调用 C++ 原型,因为 C 无法理解 C++ 类。使用 Cython 时,如果你要让你的 C 输出调用 C++ 函数,你需要小心选择编译器,或者你需要编写一个新的头文件来实现所需的包装函数,以便调用 C++。

命名空间 - 注意事项

Cython 似乎通常需要一个命名空间来保持嵌套,你可能已经在你的 C++ 代码中这样做。在非命名空间代码上创建 PXD 似乎会创建新的声明,这意味着你将因为多个符号而得到链接错误。从这些模板来看,C++ 的支持看起来非常好,并且更多的元编程习惯用法在 Cython 中可能难以表达。当多态发挥作用时,跟踪编译错误可能很困难。我强调,你应该尽可能保持你的接口简单,以便进行调试和更动态地操作!

小贴士

记住,当使用 Cython 生成 C++ 时,你需要指定 –cplus,这样它将默认输出 cythonfile.cpp。注意扩展名;我更喜欢使用 .cc 作为我的 C++ 代码,所以请确保你的构建系统正确无误。

Python distutils

与往常一样,我们也可以使用 Python distutils,但你需要指定语言,这样所需的辅助 C++ 代码才会由正确的编译器编译:

from distutils.core import setup
from Cython.Build import cythonizesetup (ext_modules = cythonize("mycython.pyx",sources = ["mysource.cc"],language = "c++",
))

现在,你可以将你的 C++ 代码编译成 Python 模块。

Python 线程和 GIL

GIL代表全局解释器锁。这意味着当你将程序链接到libpython.so并使用它时,你实际上在你的代码中拥有整个 Python 解释器。这个存在的原因是为了使并发应用程序变得非常容易。在 Python 中,你可以有两个线程同时读写同一位置,Python 会自动为你处理所有这些;与 Java 不同,在 Java 中你需要指定 Python 中的所有内容都在 GIL 之下。在讨论 GIL 及其作用时,有两个需要考虑的事情——指令原子性和读写锁。

原子指令

记住,Cython 必然会生成 C 代码,使其看起来与任何你可以导入的 Python 模块相似。所以,在底层发生的事情是,它会生成所有代码来获取 GIL 的锁,以便在运行时操作 Python 对象。让我们考虑两种执行类型。首先,你有 C 栈,它以原子方式执行,正如你所期望的那样;它不关心线程之间的同步——这留给程序员来处理。另一种是 Python,它为我们做所有的同步。当你手动使用Py_Initilize将 Python 嵌入到你的应用程序中时,这处于 C 执行之下。当涉及到调用某些内容,比如import syssys.uname,在从 C 调用的 Cython 代码中,Python GIL 会调度并阻塞多个线程同时调用以保持安全。这使得编写多线程 Python 代码非常安全。任何同时写入同一位置的错误都可以发生并被正确处理,而不是需要在 C 中的关键部分使用互斥锁

读写锁

读写锁很棒,因为在 Python 中,你很少需要关心数据上的信号量或互斥锁,除非你想要同步不同线程对资源的访问。最糟糕的情况是,你的程序可能会进入不一致的状态,但与 C/C++不同,你不会崩溃。任何对全局字典的读写操作都会按照你预期的 Python 方式处理。

Cython 关键字

好的,那么这如何影响你,更重要的是,你的代码呢?了解你的代码应该如何以及将会以并发方式执行是很重要的。如果没有理解这一点,你的调试将会很困惑。有时候 GIL 会阻碍执行,导致从 Python 到 C 代码或反之的执行出现问题。Cython 允许我们通过gilnogil关键字来控制 GIL,这通过为我们封装这个状态而变得简单得多:

Cython Python
使用 gil PyGILState_Ensure ()
使用 nogil PyGILState_Release (state)

我发现用 Python 来考虑多线程更容易从阻塞和非阻塞执行的角度来思考。在下一个例子中,我们将检查将 Web 服务器嵌入到玩具消息服务器中所需的步骤。

消息服务器

消息服务器是高度并发的示例之一;比如说,我们想在其中嵌入一个 Web 服务器来显示连接到服务器的客户端列表。如果你查看 flask,你可以看到你可以在大约八行代码中轻松地拥有一个完整的 Web 容器。

消息服务器是异步的;因此,它在 C 代码中是基于回调的。然后,这些回调可以通过 Cython 调用 Python 的 roster 对象。然后,我们可以遍历 roster 字典以获取在线客户端,并简单地作为 Web 服务返回一些 JSON,非常容易地重用 Python 代码,无需在 C/C++中编写任何内容。

在嵌入 Web 服务器时,重要的是要注意它们启动了很多线程。调用启动 Web 服务器函数将阻塞,直到它退出,这意味着如果我们首先启动 Web 服务器,消息服务器将不会并发运行。此外,由于 Web 服务器函数阻塞,如果我们在一个单独的线程上启动它,它将永远不会退出。因此,我们被迫在后台线程上运行消息服务器,我们可以从 Python 线程模块中这样做。再次强调,这是 GIL 状态变得重要的地方。如果我们用 GIL 运行消息服务器,当回调开始时,它们将崩溃或阻塞。我们可以将消息服务器包装在名为MessageServer的玩具类中:

class MessageServer(threading.Thread):_port = Nonedef __init__ (self, port):threading.Thread.__init__(self)# self.daemon = Trueself._port = port@propertydef roster(self):return _ROSTER@propertydef port(self):return self._port@staticmethoddef set_callbacks():SetConnectCallback(pyconnect_callback)SetDisconnectCallback(pydisconnect_callback)SetReadCallback(pyread_callback)def stop(self):with nogil:StopServer();def run(self):logging.info("Starting Server on localhost:%i" % self.port)MessageServer.set_callbacks()cdef int cport = self.portwith nogil:StartServer(cport)logging.info("Message Server Finished")

然后,正如你所期望的,我们可以通过运行以下代码来启动线程:

   # start libevent servermessage_server = MessageServer(port)message_server.start()

注意,我指定了with nogil。我们的 C 代码不需要 GIL,因为我们只使用纯 C 类型,并且在回调之前不接触任何 Python 运行时。一旦libevent套接字服务器异步运行,我们就可以开始启动我们的 flask Web 服务器:

from flask import Flask
from flask import jsonifyapp = Flask("DashboardExample")
dashboard = None@app.route("/")
def status():return jsonify(dashboard.roster.client_list())class Dashboard:_port = None_roster = Nonedef __init__(self, port, roster):global dashboardself._port = portself._roster = rosterdashboard = self@propertydef port(self):return self._port@propertydef roster(self):return self._rosterdef start(self):app.run(port=self.port)

Flask 非常适合编写 RESTful Web 服务。它干净、简单,最重要的是,易于使用和阅读。此服务返回客户端 roster 的 JSON 表示。由于我已经封装了 roster 对象,我使用一个简单的全局变量,以便所有 flask 路由都可以查询正确的上下文:

# start webserver
dashboard = Dashboard(port, roster)
dashboard.start()

Web 服务器现在会阻塞,直到收到终止信号。然后,它将返回,然后我们可以终止MessageServer

   # stop message server
message_server.stop()

现在,我们监听server.cfg中指定的端口:

[MessageServer]
port = 8080
webport = 8081

此 roster 对象包含一个客户端列表并处理每个回调:

class Roster:_clients = { }def handle_connect_event(self, client):""":returns True if client already exists else false"""logging.info("connect: %s" % client)if client in self._clients:return Trueself._clients[client] = Nonereturn False;def handle_disconnect_event(self, client):logging.info("disconnect: %s" % client)self._clients.pop(client, None)def handle_read_event(self, client, message):logging.info("read: %s:[%s]" % (client, message))self._clients[client] = messagedef client_list(self):return self._clients

我们按照以下方式运行服务器:

$ python server --config=config.cfg

然后,我们可以使用简单的 telnet 会话连接客户端:

$ telnet localhost 8080

我们可以输入消息,在服务器日志中看到它被处理,然后按Q退出。然后,我们可以查询 Web 服务以获取客户端列表:

$ curl -X GET localhost:8081
{"127.0.0.1": "Hello World"
}

GIL 的注意事项

使用gil时有一个需要注意的注意事项。在我们的回调中,在调用任何 Python 代码之前,我们需要在每个回调中获取 GIL;否则,我们将发生段错误并感到非常困惑。所以,如果你查看调用 Cython 函数时的每个libevent回调,你会看到以下内容:

 PyGILState_STATE gilstate_save = PyGILState_Ensure();readcb (client, (char *)data);PyGILState_Release(gilstate_save);

注意,这也在其他两个回调上调用——首先是在discb回调上:

  PyGILState_STATE gilstate_save = PyGILState_Ensure();discb (client, NULL);PyGILState_Release(gilstate_save);

最后,在连接回调中,我们必须更加小心,并这样调用它:

 PyGILState_STATE gilstate_save = PyGILState_Ensure();if (!conncb (NULL, inet_ntoa (client_addr.sin_addr))){
…}elseclose (client_fd);PyGILState_Release(gilstate_save);

我们必须这样做,因为我们使用 Cython 的nogil执行了它。在我们返回 Python 领域之前,我们需要获取gil。你真的需要戴上你的创造力帽子,看看这样一些东西,并想象你能用它做什么。例如,你可以用它作为捕获数据的方式,并使用 Twisted Web 服务器实现嵌入式 RESTful 服务器。也许,你甚至可以使用 Python JSON 将数据包装成漂亮的对象。但是,它展示了如何使用 Python 库真正扩展一个相当复杂的 C 软件组件,使其既好又具有高级性质。这使一切都非常简单且易于维护,而不是从头开始尝试做所有事情。

本地代码的单元测试

Cython 的另一个用途是单元测试共享 C 库的核心功能。如果你维护一个.pxd文件(这实际上是你需要的全部),你可以编写自己的包装类,并使用 Python 的表达力进行数据结构的可伸缩性测试。例如,我们可以按照以下方式为std::mapstd::vector编写单元测试:

from libcpp.vector cimport vectorPASSED = Falsecdef vector[int] vect
cdef int i
for i in range(10):vect.push_back(i)
for i in range(10):print vect[i]PASSED = True

然后,按照以下方式为map编写测试:

from libcpp.map cimport mapPASSED = Falsecdef map[int,int] mymap
cdef int i
for i in range (10):mymap[i] = (i + 1)for i in range (10):print mymap[i]PASSED = True

然后,如果我们将它们编译成单独的模块,我们可以简单地编写一个测试执行器:

#!/usr/bin/env python
print "Cython C++ Unit test executor"print "[TEST] std::map"
import testmap
assert testmap.PASSED
print "[PASS]"print "[TEST] std::vec"
import testvec
assert testvec.PASSED
print "[PASS]"print "Done..."

这实际上是非常简单的代码,但它展示了这个想法。如果你添加了大量的断言和致命错误处理,你就可以对你的 C/C++代码进行一些非常棒的单元测试。我们可以更进一步,使用 Python 的本地单元测试框架来实现这一点。

防止子类化

如果你使用 Cython 创建了一个扩展类型,一个你永远不会希望被子类化的类型,它是一个被 Python 类包裹的cpp类。为了防止这种情况,你可以这样做:

cimport cython@cython.final
cdef class A: passcdef class B (A): pass

当有人尝试子类化时,这个注释将引发错误:

pycode.pyx:7:5: Base class 'A' of type 'B' is final

注意,这些注释只适用于cdefcpdef函数,而不适用于正常的 Python def函数。

解析大量数据

我想通过展示解析大量 XML 的差异来尝试证明 C 类型对程序员是多么强大和原生编译的。我们可以将政府的地域数据作为这个实验的测试数据(www.epa.gov/enviro/geospatial-data-download-service)。

让我们看看这个 XML 数据的大小:

 ls -liah
total 480184
7849156 drwxr-xr-x   5 redbrain  staff   170B 25 Jul 16:42 ./
5803438 drwxr-xr-x  11 redbrain  staff   374B 25 Jul 16:41 ../
7849208 -rw-r--r--@  1 redbrain  staff   222M  9 Mar 04:27 EPAXMLDownload.xml
7849030 -rw-r--r--@  1 redbrain  staff    12M 25 Jul 16:38 EPAXMLDownload.zip
7849174 -rw-r--r--   1 redbrain  staff    57B 25 Jul 16:42 README

它太大了!在我们编写程序之前,我们需要了解一些关于这些数据结构的信息,看看我们想用它做什么。它包含设施站点地址。这似乎是这里数据的大头,所以让我们尝试使用以下纯 Python XML 解析器解析它:

from xml.etree import ElementTree as etree

代码使用etree通过以下方式解析 XML 文件:

 xmlroot = etree.parse (__xmlFile)

然后,我们通过以下方式查找头文件和设施:

headers = xmlroot.findall ('Header')
facs = xmlroot.findall ('FacilitySite')

最后,我们将它们输出到文件中:

   try:fd = open (__output, "wb")for i in facs:location = ""for y in i:if isinstance (y.text, basestring):location += y.tag + ": " + y.text + '\n'fd.write (location)# There is some dodgy unicode character# python doesn't like just ignore itexcept UnicodeEncodeError: passexcept:print "Unexpected error:", sys.exc_info()[0]raisefinally:if fd: fd.close ()

我们随后按照以下方式计时执行:

10-4-5-52:bigData redbrain$ time python pyparse.py
USEPA Geospatial DataEnvironmental Protection AgencyUSEPA Geospatial DataThis XML file was produced by US EPA and contains data specifying the locations of EPA regulated facilities or cleanups that are being provided by EPA for use by commercial mapping services and others with an interest in using this information. Updates to this file are produced on a regular basis by EPA and those updates as well as documentation describing the contents of the file can be found at URL:http://www.epa.gov/enviro
MAR-08-2013
[INFO] Number of Facilties 118421
[INFO] Dumping facilities to xmlout.datreal    2m21.936s
user    1m58.260s
sys     0m9.5800s

这段内容相当长,但让我们使用不同的 XML 实现来比较一下——Python 的 lxml。这是一个使用 Cython 实现的库,但它实现了与之前纯 Python XML 解析器相同的库:

10-4-5-52:bigData redbrain$ sudo pip install lxml

我们可以简单地将在下面的替换导入:

from lxml import etree

代码保持不变,但执行时间显著减少(通过运行 make 编译 Cython 版本,cpyparse 二进制文件是由相同的代码创建的,只是导入方式不同):

10-4-5-52:bigData redbrain$ time ./cpyparse
USEPA Geospatial DataEnvironmental Protection AgencyUSEPA Geospatial DataThis XML file was produced by US EPA and contains data specifying the locations of EPA regulated facilities or cleanups that are being provided by EPA for use by commercial mapping services and others with an interest in using this information. Updates to this file are produced on a regular basis by EPA and those updates as well as documentation describing the contents of the file can be found at URL:http://www.epa.gov/enviro
MAR-08-2013
[INFO] Number of Facilties 118421
[INFO] Dumping facilities to xmlout.datreal    0m7.874s
user    0m5.307s
sys     0m1.839s

当你只付出一点努力时,你真的可以看到使用原生代码的强大之处。为了最终确保代码相同,让我们计算我们创建的 xmlout.datMD5 校验和:

10-4-5-52:bigData redbrain$ md5 xmlout.dat xmlout.dat.cython
MD5 (xmlout.dat.python) = c2103a2252042f143489216b9c238283
MD5 (xmlout.dat.cython) = c2103a2252042f143489216b9c238283

因此,你可以看到输出完全相同,这样我们就知道没有发生任何奇怪的事情。这让人感到害怕,这可以使你的 XML 解析速度有多快;如果我们计算速度增加率,它大约快了 17.75 倍;但不要只听我的话;自己试一试。我的 MacBook 配有固态硬盘,有 4 GB 的 RAM,2 GHz 的 Core 2 Duo 处理器。

摘要

到目前为止,你已经看到了使用 Cython 可以实现的核心功能。在本章中,我们介绍了从 Cython 调用 C++ 类。你学习了如何封装模板,甚至查看了一个更复杂的应用,展示了 gilnogil 的使用。

第六章,进一步阅读 是最后一章,将回顾一些关于 Cython 的最终注意事项和用法。我将展示如何使用 Cython 与 Python 3 结合。最后,我们将探讨相关项目和我在它们使用方面的观点。

第六章. 进一步阅读

到目前为止,在这本书中,我们已经探讨了使用 Cython 的基本和高级主题。但,这并没有结束;还有更多您可以探索的主题。

概述

本章我们将讨论的其他主题包括 OpenMP 支持、Cython 预处理器以及其他相关项目。考虑其他 Python 实现,如 PyPy 或使其与 Python 3 兼容。不仅如此,还有哪些 Cython 替代方案和相关 Cython 工具可供使用。我们将探讨 numba 和 Parakeet,并查看 numpy 作为 Cython 的旗舰用法。

OpenMP 支持

OpenMP 是一种用于共享内存并行计算的语言标准 API;它在多个开源项目中使用,例如 ImageMagick (www.imagemagick.org/),旨在加快大型图像处理的速度。Cython 对此编译器扩展提供了一些支持。但是,您必须意识到您需要使用支持 OpenMP 的编译器,如 GCC 或 MSVC。Clang/LLVM 目前还没有 OpenMP 支持。这并不是解释何时以及为什么使用 OpenMP 的地方,因为它是一个庞大的主题,但您应该查看以下网站:docs.cython.org/src/userguide/parallelism.html

编译时预处理器

在编译时,类似于 C/C++,我们有 C 预处理器来决定编译什么,这主要基于条件、定义和两者的混合。在 Cython 中,我们可以使用 IFELIFELSEDEF 来复制其中的一些行为。以下代码行展示了这一示例:

DEF myConstant = "hello cython"

我们还可以从 Cython 编译器访问预定义的常量 os.uname

  • UNAME_SYSNAME

  • UNAME_NODENAME

  • UNAME_RELEASE

  • UNAME_VERSION

  • UNAME_MACHINE

我们也可以对这些内容进行条件表达式,如下所示:

IF UNAME_SYSNAME == "Windows":include "windows.pyx"
ELSE:include "unix.pyx"

您还可以在条件表达式中使用 ELIF。如果您将某些内容与 C 程序中的头文件进行比较,您将看到如何在 Cython 中复制基本的 C 预处理器行为。这为您快速了解如何在头文件中复制 C 预处理器使用提供了思路。

Python 3

将代码迁移到 Python 3 可能很痛苦,但围绕这个主题的阅读表明,人们通过仅用 Cython 编译他们的模块而不是实际迁移代码,已经成功地将他们的代码迁移到 3.x。使用 Cython,您可以通过以下方式指定输出以符合 Python 3 API:

$ cython -3 <options>

这将确保您输出的是 Python 3 内容,而不是默认的 -2 参数,该参数为 2.x 标准生成。

PyPy

PyPy 已成为标准 Python 实现的流行替代品。更重要的是,现在许多公司(从小到大)正在将其用于生产环境以提升性能和可扩展性。PyPy 与正常的 CPython 有何不同?虽然后者是一个传统的解释器,但前者是一个完整的虚拟机。它在大多数相关架构上维护了一个即时编译器后端,以进行运行时优化。

要在 PyPy 上运行 Cython 化的模块,取决于它们的 cpyext 模拟层。这还不完整,有许多不一致之处。但是,如果你勇敢并愿意尝试,它将随着每个版本的发布而变得越来越好。

AutoPXD

当涉及到编写 Cython 模块时,你大部分的工作将包括正确获取你的 pxd 声明,以便正确操作原生代码。有几个项目试图创建一个编译器,读取 C/C++ 头文件并生成你的 pxd 声明作为输出。主要问题是维护一个完全符合 C 和 C++ 解析器的编译器。我的 Google Summer of Code 项目的一部分是使用 Python 插件系统作为 GCC 的一部分,以重用 GCC 的代码来解析 C/C++ 代码。该插件可以拦截声明、类型和原型。它还没有完全准备好使用,还有其他类似的项目试图解决同样的问题。更多信息可以在github.com/cython/cython/wiki/AutoPxd找到。

Pyrex 和 Cython

Cython 是 Pyrex 的衍生产品。然而,Pyrex 更加原始,Cython 为我们提供了更强大的类型和功能,以及优化和异常处理的信心。

SWIG 和 Cython

总体来说,如果你将 SWIG (swig.org/) 视为编写原生 Python 模块的方法,你可能会被误导,认为 Cython 和 SWIG 是相似的。SWIG 主要用于编写语言绑定的包装器。例如,如果你有一些如下所示的 C 代码:

int myFunction (int, const char *){ … }

你可以按照以下方式编写 SWIG 接口文件:

/* example.i */
%module example
%{extern int myFunction (int, const char *);
...
%}

使用以下命令编译:

$ swig -python example.i

你可以像编译 Cython 输出一样编译和链接模块,因为这将生成必要的 C 代码。如果你只想创建一个基本的模块,从 Python 调用 C,这是可以的。但 Cython 为用户提供得更多。

Cython 发展得更加完善和优化,它真正理解如何与 C 类型和工作内存管理协同工作,以及如何处理异常。使用 SWIG,你无法操作数据;你只能从 Python 调用 C 端的函数。在 Cython 中,我们可以从 Python 调用 C,反之亦然。类型转换功能非常强大;不仅如此,我们还可以将 C 类型封装成真正的 Python 类,使 C 数据感觉更像是 Pythonic。

来自第五章 高级 Cython 的 XML 示例,我们能够插入 import 替换?这是由于 Cython 的类型转换,API 非常 Pythonic。我们不仅可以把 C 类型包装成 Pythonic 对象,而且还让 Cython 生成 Python 执行此操作所需的样板代码,而无需将事物包装成类。更重要的是,Cython 为用户生成了更多优化的代码。

Cython 和 NumPy

NumPy 是一个科学库,旨在提供类似于 MATLAB 的功能,MATLAB 是一个付费的专有数学包。由于你可以使用 C 类型从高度计算密集型的代码中获得更多性能,NumPy 在 Cython 用户中非常受欢迎。在 Cython 中,你可以如下导入这个库:

import numpy as np
cimport numpy as npnp.import_array()

你可以如下访问完整的 Python API:

np.PyArray_ITER_NOTDONE

因此,你可以在 API 的一个非常本地区域与迭代器集成。这允许 NumPy 用户在通过以下方式使用本地类型时获得很多速度:

cdef double * val = (<double*>np.PyArray_MultiIter_DATA(it, 0))[0]

我们可以将数组中的数据转换为 double,在 Cython 中它是一个 cdef 类型,现在可以与之一起工作。有关更多信息以及 NumPy 教程,请访问 github.com/cython/cython/wiki/tutorials-numpy

Numba 与 Cython 的比较

Numba 是另一种让你的 Python 代码几乎成为宿主系统的本地代码的方法,通过无缝输出要在 LLVM 上运行的代码。Numba 使用以下装饰器等:

@autojit
def myFunction (): ...

Numba 还与 NumPy 集成。总的来说,这听起来很棒。与 Cython 不同,你只需将装饰器应用于纯 Python 代码,它为你做所有事情,但你可能会发现优化会更少,也不那么强大。

Numba 并没有像 Cython 那样与 C/C++ 集成。如果你想让它集成,你需要使用 外部函数接口FFI)来包装调用。你还需要在 Python 代码中以非常抽象的方式定义结构体并与 C 类型一起工作,以至于与 Cython 相比,你实际上几乎没有多少控制权。

Numba 主要由装饰器组成,例如来自 Cython 的 @locals。但最终,所有这些创建的只是即时编译的函数,具有适当的本地函数签名。由于你可以指定函数调用的类型,这应该会在调用和从函数返回数据时提供更本地的速度。我认为,与 Cython 相比,你将获得的优化将非常有限,因为你可能需要很多抽象来与本地代码通信;尽管如此,调用很多函数可能是一种更快的技术。

仅作参考,LLVM 是一个低级虚拟机;它是一个编译器开发基础设施,项目可以使用它作为即时编译器。该基础设施可以扩展以运行各种事物,例如纯 Java 字节码,甚至通过 Numba 运行 Python。它几乎可以用于任何目的,并提供了一个良好的 API 用于开发。与 GCC(一个编译时编译器基础设施)相反,GCC 在代码运行之前会提前执行大量的静态分析,LLVM 允许代码在运行时进行更改。

小贴士

如需了解更多关于 Numba 和 LLVM 的信息,您可以参考以下链接中的任何一个:

numba.pydata.org/

llvm.org/

Parakeet 和 Numba

Parakeet 是另一个与 Numba 一起工作的项目,它为使用大量嵌套循环和并行性的 Python 代码添加了非常具体的优化。与 OpenMP 类似,它真的很酷,Numba 也需要您在代码上使用注解来完成所有这些工作。缺点是您不会神奇地优化任何 Python 代码,Parakeet 所做的优化是针对非常具体的代码集。

相关链接

一些有用的参考链接:

  • github.com/cython/cython/wiki/FAQ

  • github.com/cython/cython/wiki

  • cython.org/

  • www.cosc.canterbury.ac.nz/greg.ewing/python/Pyrex/

  • swig.org/

  • www.numpy.org/

  • wiki.cython.org/tutorials/numpy

  • en.wikipedia.org/wiki/NumPy

  • llvm.org/

  • numba.pydata.org/

  • numba.pydata.org/numba-doc/0.9/interface_c.html

  • gcc.gnu.org/

摘要

如果您已经阅读到这里,那么您现在应该对 Cython 非常熟悉,以至于您可以使用 C 绑定将其嵌入,甚至可以使一些纯 Python 代码更加高效。我已经向您展示了如何将 Cython 应用于实际的开源项目,甚至如何使用 Twisted Web 服务器扩展原生软件!正如我在整本书中一直说的那样,这使得 C 感觉到似乎有无穷无尽的可能性来控制逻辑,或者您可以使用大量的 Python 模块来扩展系统。感谢您的阅读。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/907123.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

印度尼西亚股票数据API对接实现

环境准备 首先安装必要的依赖包: pip install requests websocket-client pandas numpy基础配置 import requests import json import websocket import threading import time from datetime import datetime# API配…

OpenBMB 发布无分词器 TTS VoxCPM;儿童口语硬件 Dex 融资 480 万美元:拍摄真实物体,对话学习外语丨日报

开发者朋友们大家好:这里是 「RTE 开发者日报」,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的技术」、「有亮点的产品」、「有思考的文章」、「有态度…

一天一款实用的AI工具,第1期,AI标题生成工具

本期介绍的是一款专业的标题生成工具,它能帮你产出高质量标题,让点击率提升,让内容被看见。现实问题 在内容创作的世界里,有句话特别扎心: 好的标题=成功的一半。 很多创作者都遇到过这样的困境: 花了一下午写好…

重组蛋白表达避坑指南

重组蛋白表达避坑指南重组蛋白表达是分子生物学、生物技术以及生物医学研究中非常基础却经常“出问题”的环节。一个合适的蛋白表达方案,不仅要能产生足够的产量,还要确保蛋白正确折叠、具有功能、具有良好的纯度与稳…

易被忽略的vim中视图模式

常见的都是vim三种模式,但视图模式也不可忽略,主要进行批量操作在 Vim 中,可视模式(Visual Mode)是一种强大的文本选择和编辑模式,允许你高亮选中一段文本,然后对其进行操作(如复制、删除、替换、注释等)。 一…

详细介绍:智慧校园统一身份认证中心:一个账号畅行校园内外

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

电商核心业务 - 指南

电商核心业务 - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "…

一言

一些日常的感想,为了节约时间,为了不暴露太多东西,为了不挑起矛盾,内容会很简洁,在合适的时候公布详情。9.17 说好的向阳而生呢?冷静啊,兄弟。 9.18 你们不相信我,我必将证明我,夺回属于我的荣耀。

ai

https://qsqs.life/login?redirect=/system/dashboard本文来自博客园,作者:zjxgdq,转载请注明原文链接:https://www.cnblogs.com/zjxzhj/p/19098509

LlamaIndex 项目深度技术分析 - 详解

LlamaIndex 项目深度技术分析 - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monac…

苏州才是最美的烟雨江南,苏州游玩必去的10大景点

苏州才是最美的烟雨江南,苏州游玩必去的10大景点 蜘蛛指南 关注2024-05-22 16:22 北京 来源:澎湃新闻澎湃号湃客 字号苏州人间天堂 最美的烟雨江南 苏州,一个极具江南风情的城市,既有园林之美,也有诗情画意,也是…

深入解析:css消除图片下的白边

深入解析:css消除图片下的白边pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco&quo…

linux增加网卡ip地址

linux增加网卡ip地址example ip addr add 192.168.5.124/24 dev eth0 label eth0:5 ifconfig eth0:5 up ip addr del 192.168.1.100/24 dev eth0 example ip addr add 192.168.10.199/24 dev eth0 label eth0:10 route…

Python 包与环境管理简史:从混乱到优雅

自动包管理工具的先驱:easy_install 在一切规范化工具出现之前,Python 的包管理是相当原始的。开发者们需要把第三方库的源码下载下来,手动放到项目目录里。 为了解决自动安装包的问题,easy_install 应运而生。 20…

qoj853 Flat Organization

SOLUTION FROM WUMIN4 题意 给出一个 \(n\) 个点的带权竞赛图(定向完全图),你可以进行任意次操作,每次操作反转一条边,代价为边权,求使得图强连通的最小代价和与方案,或输出无解。 \(n\le 2000\)。 思路 我们先…

实用指南:Chromium 138 编译指南 macOS 篇:Xcode 与开发工具安装配置(二)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

shell命令中循环执行操作的命令

shell命令中循环执行操作的命令reference: for i in $(seq 1 10000); do echo "Iteration $i" && echo "Iteration $i"; done for i in $(seq 1 10000); do cat /sys/class/net/eth0/carr…

2025年9月中国数据库排行榜:达梦挺进榜眼位,崖山首入前十强

9月墨天轮排行榜解读已出炉!本月前十变动较大,老将突围、新秀崛起,达梦凭借强劲势头跃升至第二位、TiDB排名上升、崖山首次闯入前十,此外还有一些产品表现亮眼!本月墨天轮社区的中国数据库排行榜再起波澜。达梦凭…

基于QEMU模拟器搭建Builtroot下的QT开发环境

基于QEMU模拟器搭建Builtroot下的QT开发环境https://www.cnblogs.com/arnoldlu/p/17250728.html

OpenSSH漏洞修复

前期准备 (先使用Telnet远程连接工具,连接服务器,确保Telnet连接正常,SSH连接后进行漏洞修复升级(防止修复失败,导致远程连接无法连接时,可以通过另一个远程工具连接进行恢复) telnet安装与开启:https://www.…