17、Python 教程 - 扩展 Python
Python 什么都能做,真的是这样。这门语言功能强大,但有时候 速度有点慢。
鱼和熊掌兼得
本章讨论确实需要进一步提升速度的情形。在这种情况下,最佳的解决方案可能不是完全转向 C 语言(或其他中低级语言),建议你采用下面的方法(这可满足众多的速度至上需求)。
**1、**使用 Python 开发原型(有关原型开发的详细信息,请参阅第 19 章);**2、**对程序进行性能分析以找出瓶颈(有关测试,请参阅第 16 章);3. 使用 C(或者 C++、C#、Java、Fortran 等)扩展重写瓶颈部分。这样得到的架构(包含一个或多个 C 语言组件的 Python 框架)将非常强大,因为它兼具这两门语言的优点。
简单易行的方式:Jython 和 IronPython
使用 Jython(http://jython.org)或 IronPython(http://ironpython.net),可轻松地使用原生模块来扩展 Python。Jython 和 IronPython 能够让你访问底层语言中的模块和类(对 Jython 来说,底层语言为 Java;对 IronPython 来说,为 C#和其他.NET 语言)
一个简单的 Java 类(JythonTest.java)
text
public class JythonTest {
public void greeting() {
System.out.println("Hello, world!")
}
}
使用 Java 编译器(如 javac)来编译这个类。javac JythonTest.java
编译这个类后,启动 Jython(并将.class 文件放到当前目录或 Java CLASSPATH 包含的目录中)CLASSPATH=JythonTest.class jython
然后,就可直接导入这个类了。
text
import JythonTest
test = JythonTest()
test.greeting()#输出结果如下:
'''
Hello, world!
'''
一个简单的 C#类(IronPythonTest.cs)
text
using System;
namespace FePyTest {
public class IronPythonTest {
public void greeting() {
Console.WriteLine("Hello, world!");
}
}
}
选择的编译器来编译这个类,对于 Microsoft .NET,命令如下:csc.exe /t:library IronPythonTest.cs 要在 IronPython 中使用这个类,一种方法是将其编译为动态链接库(DLL),并根据需要修改相关的环境变量(如 PATH),然后就应该能够像下面这样使用它了(这里使用的是 IronPython 交互式解释器):
text
import clr
clr.AddReferenceToFile("IronPythonTest.dll")
import FePyTest
f = FePyTest.IronPythonTest()
f.greeting()
编写 C 语言扩展
扩展 Python 通常意味着扩展 CPython------使用编程语言 C 实现的 Python 标准版
C 语言的动态性不如 Java 和 C#,而且对 Python 来说,编译后的 C 语言代码也不那么容易理解。因此,使用 C 语言编写 Python 扩展时,必须遵循严格的 API。
其他方法使用 Cpython,有很多工具可帮助提高程序的速度,这是通过生成和使用 C 语言库或提高 Python 代码的速度实现的。
| 工具 | 描述 |
|---|---|
| Cython(http://cython.org) | 这其实是一个 Python 编译器!它还提供了扩展的 Cython 语言,该语言基于 Greg Ewing 开发的项目 Pyrex,让你能够使用类似于 Python 的语法添加类型声明和定义 C 类型。因此,它的效率非常高,并且能够很好地与 C 扩展模块(包括 Numpy)交互。 |
| PyPy(http://pypy.org) | 这是一个雄心勃勃而有远见的 Python 实现------使用的是 Python。这种实现好像会慢如蜗牛,但通过极其复杂的代码分析和编译,其性能实际上超过了 CPython。其官网指出:"有传言说 PyPy 的秘密目标是在速度上超过 C 语言,这是无稽之谈,不是吗?"PyPy 的核心是 RPython------一种受限的 Python 方言。RPython 擅长自动类型推断等,可转换为静态语言、机器码和其他动态语言(如 JavaScript)。 |
| Weave(http://scipy.org) | SciPy 发布版的一部分,也有单独的安装包。这个工具让你能够在 Python 代码中以字符串的方式直接包含 C 或 C++ 代码,并无缝地编译和执行这些代码。例如,要快速计算一些数学表达式,就可使用这个工具。Weave 还可提高使用数字数组的表达式的计算速度。 |
| NumPy(http://numpy.org) | NumPy 让你能够使用数字数组,这对分析各种形式的数值数据(从股票价值到天文图像)很有帮助。NumPy 的优点之一是接口简单,无需显式地指定众多低级操作。然而,NumPy 的主要优点是速度快。对数字数组中的每个元素执行很多常见操作时,速度都比使用列表和 for 循环执行同样的操作快得多,这是因为隐式循环是直接使用 C 语言实现的。数字数组能够很好地与 Cython 和 Weave 协同工作。 |
| ctypes(https://docs.python.org/library/ctypes.html) | 模块 ctypes 最初是 Thomas Heller 开发的一个项目,但现在包含在标准库中。它采用直截了当的方法------能够导入既有(共享)的 C 语言库。虽然存在一些限制,但这可能是访问 C 语言代码的最简单方式之一。不需要包装器,也不需要特殊 API,只需将库导入就可使用。 |
| subprocess(https://docs.python.org/3/library/subprocess.html) | 模块 subprocess 包含在标准库中(标准库中还有一些较老的模块和函数提供了类似的功能)。它让你能够在 Python 中运行外部程序,并通过命令行参数以及标准输入、输出和错误流与它们通信。如果对速度要求极高的代码可使用几个批处理作业来完成大部分工作,启动外部程序并与之通信所需的时间将很短。在这种情况下,将 C 语言代码放在独立的程序中并将其作为子进程运行很可能是最整洁的解决方案。 |
| PyCXX(http://cxx.sourceforge.net) | 以前名为 CXX 或 CXX/Objects,是一组帮助使用 C++ 编写 Python 扩展的工具。例如,它提供了良好的引用计数支持,可减少犯错的机会。 |
| SIP(http://www.riverbankcomputing.co.uk/software/sip) | SIP 最初是一个开发 GUI 包 PyQt 的工具,包含一个代码生成器和一个 Python 模块。它像 SWIG 那样使用规范文件。 |
| Boost.Python(http://www.boost.org/libs/python/doc) | Boost.Python 让 Python 和 C++ 能够无缝地互操作,可为你解决引用计数和在 C++ 中操作 Python 对象提供极大的帮助。一种使用它的主要方式是,以类似于 Python 的方式编写 C++ 代码(Boost.Python 中的宏为此提供了支持),再使用你喜欢的 C++ 编译器将这些代码编译成 Python 扩展。它虽然与 SWIG 有天壤之别,却能很好地替代 SWIG,因此很值得你研究研究。 |
SWIG
SWIG(http://www.swig.org)指的是简单包装器和接口生成器(simple wrapper and interfacegenerator),是一个适用于多种语言的工具。一方面,它够使用 C 或 C++ 编写扩展代码;另一方面,它自动包装这些代码,能够在 Tcl、Python、Perl、Ruby 和 Java 等高级语言中使用它们。
SWIG 使用起来很简单,前提条件是有一些 C 语言代码
1,用法(1),为代码编写一个接口文件。这很像 C 语言头文件(在比较简单的情况下,可直接使用现有的头文件)。(2),对接口文件运行 SWIG,以自动生成一些额外的 C 语言代码(包装器代码)。(3),将原来的 C 语言代码和生成的包装器代码一起编译,以生成共享库。
2,回文回文(palindrome;如 I prefer pi)是忽略空格、标点等后正着读和反着读一样的句子。
一个简单的检测回文的 C 语言函数(palindrome.c)
text
#include <string.h>
int is_palindrome(char *text) {
int i, n=strlen(text);
for (i = 0; I <= n/2; ++i) {
if (text[i] != text[n-i-1]) return 0;
}
return 1;
}
检测回文的 Python 函数
text
def is_palindrome(text):
n = len(text)
for i in range(len(text) // 2):
if text[i] != text[n-i-1]:
return False
return True
3,接口文件假设代码存储在文件 palindrome.c 中,现在应该在文件 palindrome.i 中添加接口描述。
如果定义一个头文件(这里为 palindrome.h),SWIG 可能能够从中获取所需的信息。
在接口文件中,只是声明要导出的函数(和变量),就像在头文件中一样。
在接口文件的开头,有一个由 %{和 %}界定的部分,可在其中指定要包含的头文件(这里为 string.h),%module 声明,用于指定模块名。
回文检测库的接口(palindrome.i)
text
%module palindrome
%{
#include <string.h>
%}
extern int is_palindrome(char *text);
4,运行 SWIG 运行 SWIG 时,需要将接口文件(也可以是头文件)作为参数 $swig -python palindrome.i
这将生成两个新文件,分别是 palindrome_wrap.c 和**palindrome.py**。
5,编译、链接和使用在 Linux 中使用编译器 gcc
text
$ gcc -c palindrome.c
$ gcc -I$PYTHON_HOME -I$PYTHON_HOME/Include -c palindrome_wrap.c
$ gcc -shared palindrome.o palindrome_wrap.o -o _palindrome.so
将得到一个很有用的文件_palindrome.so。它就是共享库,可直接导入到 Python 中(条件是它位于 PYTHONPATH 包含的目录中,如当前目录中)
text
import _palindrome
dir(_palindrome)#结果为:['__doc__', '__file__', '__name__', 'is_palindrome']
_palindrome.is_palindrome('ipreferpi')#结果为:1
_palindrome.is_palindrome('notlob')#结果为:0
6,穿越编译器"魔法森林"的捷径通过使用 Setuptools,直接支持 SWIG,让你无需手工运行 SWIG:只需编写代码和接口文件,再运行安装脚本。
手工编写扩展
SWIG 在幕后做了很多工作,但并非每项工作都是绝对必要的。如果你愿意,可自己编写包装代码,也可在 C 语言代码中直接使用 Python C API。Python/C API 参考手册标准库参考手册
1,引用计数在 Python 中,内存管理是自动完成的:你只管创建对象,当你不再使用时它们就会消失。在 C 语言中,必须显式地释放不再使用的对象(更准确地说是内存块),否则程序占用的内存将越来越多,这称为内存泄漏(memory leak)。可使用 Python 在幕后使用的内存管理工具,其中之一就是引用计数。一个对象只要被代码引用(在 C 语言中是有指向它的指针),就不应将其释放。
为将对象的引用计数加 1 和减 1,可使用两个宏,分别是 Py_INCREF 和 Py_DECREF。
- 对象不归你所有,但指向它的引用归你所有。一个对象的引用计数是指向它的引用的数量。
- 对于归你所有的引用,你必须负责在不再需要它时调用 Py_DECREF。
- 对于你暂时借用的引用,不应在借用完后调用 Py_DECREF,因为这是引用所有者的职责。
- 可通过调用 Py_INCREF 将借来的引用变成自己的。这将创建一个新引用,而借来的引用依然归原来的所有者所有。
- 通过参数收到对象后,要转移所有权(如将其存储起来)还是仅仅借用完全由你决定,但应清楚地说明。如果函数将在 Python 中调用,完全可以只借用,因为对象在整个函数调用期间都存在。然而,如果函数将在 C 语言中调用,就无法保证对象在函数调用期间都存在,因此可能应该创建自己的引用,并在使用完毕后将其释放。
再谈垃圾收集 引用计数是一种垃圾收集方式,其中的术语"垃圾"指的是程序不再使用的对象。循环垃圾,即两个对象相互引用对方(导致它们的引用计数不为 0),但没有其他的对象引用它们。
2,扩展框架必须先包含头文件 Python.h,再包含其他标准头文件。
text
#include <Python.h>
static PyObject *somename(PyObject *self, PyObject *args) {
PyObject *result;
Py_INCREF(result); /* 仅当需要时才这样做!*/
return result;
}
int PyArg_ParseTuple(PyObject *args, char *format, ...);
3,回文另一个回文检查示例(palindrome2.c)
text
#include <Python.h>
static PyObject *is_palindrome(PyObject *self, PyObject *args) {
int i, n;
const char *text;
int result;
if (!PyArg_ParseTuple(args, "s", &text)) {
return NULL;
}
n=strlen(text);
result = 1;
for (i = 0; i <= n/2; ++i) {
if (text[i] != text[n-i-1]) {
result = 0;
break;
}
}
return Py_BuildValue("i", result); /* "i"表示一个整数:*/
}
static PyMethodDef PalindromeMethods[] = {
/* 方法/函数列表:*/
{
"is_palindrome", is_palindrome, METH_VARARGS, "Detect palindromes"},{
NULL, NULL, 0, NULL}
};
static struct PyModuleDef palindrome =
{
PyModuleDef_HEAD_INIT,
"palindrome", /* 模块名 */
"", /* 文档字符串 */
-1, /*存储在全局变量中的信号状态 */
PalindromeMethods
};
/* 初始化模块的函数:*/
PyMODINIT_FUNC PyInit_palindrome(void)
{
return PyModule_Create(&palindrome);
}
小结
| 概念 | 描述 |
|---|---|
| 扩展理念 | Python 扩展的主要用途有两个------利用既有(遗留)代码和提高瓶颈部分的速度。从头开始编写代码时,请尝试使用 Python 建立原型,找出其中的瓶颈并在需要时使用扩展来替换它们。预先将潜在的瓶颈封装起来大有裨益。 |
| Jython 和 IronPython | 对这些 Python 实现进行扩展很容易,使用底层语言(对于 Jython,为 Java;对于 IronPython,为 C#和其他.NET 语言)以库的方式实现扩展后,就可在 Python 中使用它们了。 |
| 扩展方法 | 有很多用于扩展代码或提高其速度的工具,有的让你更轻松地在 Python 程序中嵌入 C 语言代码,有的可提高数字数组操作等常见运算的速度,有的可提高 Python 本身的速度。这样的工具包括 SWIG、Cython、Weave、NumPy、ctypes 和 subprocess。 |
| SWIG | SWIG 是一款自动为 C 语言库生成包装代码的工具。包装代码自动处理 Python CAPI,使你不必自己去做这样的工作。使用 SWIG 是最简单、最流行的扩展 Python 的方式之一。 |
| 使用 Python/C API | 可手工编写可作为共享库直接导入到 Python 中的 C 语言代码。为此,必须遵循 Python/C API:对于每个函数,你都需要负责完成引用计数、提取参数以及创建返回值等工作;另外,还需编写将 C 语言库转换为模块的代码,包括列出模块中的函数以及创建模块初始化函数。 |
本章介绍的新函数
| 函数 | 描述 |
|---|---|
| Py_INCREF(obj) | 将 obj 的引用计数加 1 |
| Py_DECREF(obj) | 将 obj 的引用计数减 1 |
| PyArg_ParseTuple(args, fmt, ...) | 提取位置参数 |
| PyArg_ParseTupleAndKeywords(args, kws, fmt, kwlist) | 提取位置参数和关键字参数 |
| PyBuildValue(fmt, value) | 根据 C 语言值创建 PyObject |