第十七章
Python什么都能做,真的是這樣。這門語言功能強大,但有時候速度有點慢
。
魚和熊掌兼得
本章討論確實需要進一步提升速度的情形。在這種情況下,最佳的解決方案可能不是完全轉向C語言(或其他中低級語言),建議你采用下面的方法(這可滿足眾多的速度至上需求)。
- 使用Python開發原型(有關原型開發的詳細信息,請參閱第19章)。
- 對程序進行性能分析以找出瓶頸(有關測試,請參閱第16章)。
- 使用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)
public class JythonTest {public void greeting() {System.out.println("Hello, world!")}
}
使用Java編譯器(如javac)來編譯這個類。javac JythonTest.java
編譯這個類后,啟動Jython(并將.class文件放到當前目錄或Java CLASSPATH包含的目錄中)
CLASSPATH=JythonTest.class jython
然后,就可直接導入這個類了。
import JythonTest
test = JythonTest()
test.greeting()#輸出結果如下:
'''
Hello, world!
'''
一個簡單的C#類(IronPythonTest.cs)
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交互式解釋器):
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)
#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函數
def is_palindrome(text):n = len(text)for i in range(len(text) // 2):if text[i] != text[n-i-1]:return Falsereturn True
3,接口文件
假設代碼存儲在文件palindrome.c中,現在應該在文件palindrome.i中添加接口描述。
如果定義一個頭文件(這里為palindrome.h),SWIG可能能夠從中獲取所需的信息。
在接口文件中,只是聲明要導出的函數(和變量),就像在頭文件中一樣。
在接口文件的開頭,有一個由%{和%}界定的部分,可在其中指定要包含的頭文件(這里為string.h),%module聲明,用于指定模塊名。
回文檢測庫的接口(palindrome.i)
%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
$ 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包含的目錄中,如當前目錄中)
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,再包含其他標準頭文件。
#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)
#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 |