本章著眼于從 Python 訪問 C 代碼的問題。許多 Python 內(nèi)置庫是用 C 寫的, 訪問 C 是讓 Python 的對現(xiàn)有庫進(jìn)行交互一個重要的組成部分。 這也是一個當(dāng)你面臨從 Python 2 到 Python 3 擴(kuò)展代碼的問題。 雖然 Python 提供了一個廣泛的編程 API,實際上有很多方法來處理 C 的代碼。 相比試圖給出對于每一個可能的工具或技術(shù)的詳細(xì)參考, 我么采用的是是集中在一個小片段的 C++ 代碼,以及一些有代表性的例子來展示如何與代碼交互。 這個目標(biāo)是提供一系列的編程模板,有經(jīng)驗的程序員可以擴(kuò)展自己的使用。
這里是我們將在大部分秘籍中工作的代碼:
/* sample.c */_method
#include <math.h>
/* Compute the greatest common divisor */
int gcd(int x, int y) {
int g = y;
while (x > 0) {
g = x;
x = y % x;
y = g;
}
return g;
}
/* Test if (x0,y0) is in the Mandelbrot set or not */
int in_mandel(double x0, double y0, int n) {
double x=0,y=0,xtemp;
while (n > 0) {
xtemp = x*x - y*y + x0;
y = 2*x*y + y0;
x = xtemp;
n -= 1;
if (x*x + y*y > 4) return 0;
}
return 1;
}
/* Divide two numbers */
int divide(int a, int b, int *remainder) {
int quot = a / b;
*remainder = a % b;
return quot;
}
/* Average values in an array */
double avg(double *a, int n) {
int i;
double total = 0.0;
for (i = 0; i < n; i++) {
total += a[i];
}
return total / n;
}
/* A C data structure */
typedef struct Point {
double x,y;
} Point;
/* Function involving a C data structure */
double distance(Point *p1, Point *p2) {
return hypot(p1->x - p2->x, p1->y - p2->y);
}
這段代碼包含了多種不同的 C 語言編程特性。 首先,這里有很多函數(shù)比如 gcd()
和 is_mandel()
。 divide()
函數(shù)是一個返回多個值的 C 函數(shù)例子,其中有一個是通過指針參數(shù)的方式。 avg()
函數(shù)通過一個 C 數(shù)組執(zhí)行數(shù)據(jù)聚集操作。Point
和 distance()
函數(shù)涉及到了 C 結(jié)構(gòu)體。
對于接下來的所有小節(jié),先假定上面的代碼已經(jīng)被寫入了一個名叫“sample.c”的文件中, 然后它們的定義被寫入一個名叫“sample.h”的頭文件中, 并且被編譯為一個庫叫“l(fā)ibsample”,能被鏈接到其他 C 語言代碼中。 編譯和鏈接的細(xì)節(jié)依據(jù)系統(tǒng)的不同而不同,但是這個不是我們關(guān)注的。 如果你要處理 C 代碼,我們假定這些基礎(chǔ)的東西你都掌握了。
你有一些 C 函數(shù)已經(jīng)被編譯到共享庫或 DLL 中。你希望可以使用純 Python 代碼調(diào)用這些函數(shù), 而不用編寫額外的 C 代碼或使用第三方擴(kuò)展工具。
對于需要調(diào)用 C 代碼的一些小的問題,通常使用 Python 標(biāo)準(zhǔn)庫中的 ctypes
模塊就足夠了。 要使用ctypes
,你首先要確保你要訪問的 C 代碼已經(jīng)被編譯到和 Python 解釋器兼容 (同樣的架構(gòu)、字大小、編譯器等)的某個共享庫中了。 為了進(jìn)行本節(jié)的演示,假設(shè)你有一個共享庫名字叫 libsample.so
,里面的內(nèi)容就是15章介紹部分那樣。 另外還假設(shè)這個 libsample.so
文件被放置到位于sample.py
文件相同的目錄中了。
要訪問這個函數(shù)庫,你要先構(gòu)建一個包裝它的 Python 模塊,如下這樣:
# sample.py
import ctypes
import os
# Try to locate the .so file in the same directory as this file
_file = 'libsample.so'
_path = os.path.join(*(os.path.split(__file__)[:-1] + (_file,)))
_mod = ctypes.cdll.LoadLibrary(_path)
# int gcd(int, int)
gcd = _mod.gcd
gcd.argtypes = (ctypes.c_int, ctypes.c_int)
gcd.restype = ctypes.c_int
# int in_mandel(double, double, int)
in_mandel = _mod.in_mandel
in_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)
in_mandel.restype = ctypes.c_int
# int divide(int, int, int *)
_divide = _mod.divide
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
_divide.restype = ctypes.c_int
def divide(x, y):
rem = ctypes.c_int()
quot = _divide(x, y, rem)
return quot,rem.value
# void avg(double *, int n)
# Define a special type for the 'double *' argument
class DoubleArrayType:
def from_param(self, param):
typename = type(param).__name__
if hasattr(self, 'from_' + typename):
return getattr(self, 'from_' + typename)(param)
elif isinstance(param, ctypes.Array):
return param
else:
raise TypeError("Can't convert %s" % typename)
# Cast from array.array objects
def from_array(self, param):
if param.typecode != 'd':
raise TypeError('must be an array of doubles')
ptr, _ = param.buffer_info()
return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
# Cast from lists/tuples
def from_list(self, param):
val = ((ctypes.c_double)*len(param))(*param)
return val
from_tuple = from_list
# Cast from a numpy array
def from_ndarray(self, param):
return param.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
DoubleArray = DoubleArrayType()
_avg = _mod.avg
_avg.argtypes = (DoubleArray, ctypes.c_int)
_avg.restype = ctypes.c_double
def avg(values):
return _avg(values, len(values))
# struct Point { }
class Point(ctypes.Structure):
_fields_ = [('x', ctypes.c_double),
('y', ctypes.c_double)]
# double distance(Point *, Point *)
distance = _mod.distance
distance.argtypes = (ctypes.POINTER(Point), ctypes.POINTER(Point))
distance.restype = ctypes.c_double
如果一切正常,你就可以加載并使用里面定義的 C 函數(shù)了。例如:
>>> import sample
>>> sample.gcd(35,42)
7
>>> sample.in_mandel(0,0,500)
1
>>> sample.in_mandel(2.0,1.0,500)
0
>>> sample.divide(42,8)
(5, 2)
>>> sample.avg([1,2,3])
2.0
>>> p1 = sample.Point(1,2)
>>> p2 = sample.Point(4,5)
>>> sample.distance(p1,p2)
4.242640687119285
>>>
本小節(jié)有很多值得我們詳細(xì)討論的地方。 首先是對于 C 和 Python 代碼一起打包的問題,如果你在使用 ctypes
來訪問編譯后的C代碼, 那么需要確保這個共享庫放在 sample.py
模塊同一個地方。 一種可能是將生成的 .so
文件放置在要使用它的 Python 代碼同一個目錄下。 我們在 recipe—sample.py
中使用 __file__
變量來查看它被安裝的位置, 然后構(gòu)造一個指向同一個目錄中的 libsample.so
文件的路徑。
如果 C 函數(shù)庫被安裝到其他地方,那么你就要修改相應(yīng)的路徑。 如果 C 函數(shù)庫在你機(jī)器上被安裝為一個標(biāo)準(zhǔn)庫了, 那么可以使用ctypes.util.find_library()
函數(shù)來查找:
>>> from ctypes.util import find_library
>>> find_library('m')
'/usr/lib/libm.dylib'
>>> find_library('pthread')
'/usr/lib/libpthread.dylib'
>>> find_library('sample')
'/usr/local/lib/libsample.so'
>>>
一旦你知道了 C 函數(shù)庫的位置,那么就可以像下面這樣使用 ctypes.cdll.LoadLibrary()
來加載它, 其中 _path
是標(biāo)準(zhǔn)庫的全路徑:
_mod = ctypes.cdll.LoadLibrary(_path)
函數(shù)庫被加載后,你需要編寫幾個語句來提取特定的符號并指定它們的類型。 就像下面這個代碼片段一樣:
# int in_mandel(double, double, int)
in_mandel = _mod.in_mandel
in_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)
in_mandel.restype = ctypes.c_int
在這段代碼中,.argtypes
屬性是一個元組,包含了某個函數(shù)的輸入按時, 而 .restype
就是相應(yīng)的返回類型。ctypes
定義了大量的類型對象(比如 c_double, c_int, c_short, c_float 等), 代表了對應(yīng)的 C 數(shù)據(jù)類型。如果你想讓 Python 能夠傳遞正確的參數(shù)類型并且正確的轉(zhuǎn)換數(shù)據(jù)的話, 那么這些類型簽名的綁定是很重要的一步。如果你沒有這么做,不但代碼不能正常運(yùn)行, 還可能會導(dǎo)致整個解釋器進(jìn)程掛掉。 使用 ctypes 有一個麻煩點的地方是原生的 C 代碼使用的術(shù)語可能跟 Python 不能明確的對應(yīng)上來。 divide()
函數(shù)是一個很好的例子,它通過一個參數(shù)除以另一個參數(shù)返回一個結(jié)果值。 盡管這是一個很常見的 C 技術(shù),但是在 Python 中卻不知道怎樣清晰的表達(dá)出來。 例如,你不能像下面這樣簡單的做:
>>> divide = _mod.divide
>>> divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
>>> x = 0
>>> divide(10, 3, x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ctypes.ArgumentError: argument 3: <class 'TypeError'>: expected LP_c_int
instance instead of int
>>>
就算這個能正確的工作,它會違反 Python 對于整數(shù)的不可更改原則,并且可能會導(dǎo)致整個解釋器陷入一個黑洞中。 對于涉及到指針的參數(shù),你通常需要先構(gòu)建一個相應(yīng)的 ctypes 對象并像下面這樣傳進(jìn)去:
>>> x = ctypes.c_int()
>>> divide(10, 3, x)
3
>>> x.value
1
>>>
在這里,一個 ctypes.c_int
實例被創(chuàng)建并作為一個指針被傳進(jìn)去。 跟普通 Python 整形不同的是,一個c_int
對象是可以被修改的。.value
屬性可被用來獲取或更改這個值。
對于那些不像 Python 的 C 調(diào)用,通??梢詫懸粋€小的包裝函數(shù)。 這里,我們讓 divide()
函數(shù)通過元組來返回兩個結(jié)果:
# int divide(int, int, int *)
_divide = _mod.divide
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
_divide.restype = ctypes.c_int
def divide(x, y):
rem = ctypes.c_int()
quot = _divide(x,y,rem)
return quot, rem.value
avg()
函數(shù)又是一個新的挑戰(zhàn)。C 代碼期望接受到一個指針和一個數(shù)組的長度值。 但是,在 Python 中,我們必須考慮這個問題:數(shù)組是啥?它是一個列表?一個元組? 還是 array
模塊中的一個數(shù)組?還是一個 numpy
數(shù)組?還是說所有都是? 實際上,一個 Python “數(shù)組”有多種形式,你可能想要支持多種可能性。
DoubleArrayType
演示了怎樣處理這種情況。 在這個類中定義了一個單個方法 from_param()
。 這個方法的角色是接受一個單個參數(shù)然后將其向下轉(zhuǎn)換為一個合適的 ctypes 對象 (本例中是一個 ctypes.c_double
的指針)。 在 from_param()
中,你可以做任何你想做的事。 參數(shù)的類型名被提取出來并被用于分發(fā)到一個更具體的方法中去。 例如,如果一個列表被傳遞過來,那么typename
就是 list
, 然后 from_list
方法被調(diào)用。
對于列表和元組,from_list
方法將其轉(zhuǎn)換為一個 ctypes
的數(shù)組對象。 這個看上去有點奇怪,下面我們使用一個交互式例子來將一個列表轉(zhuǎn)換為一個 ctypes
數(shù)組:
>>> nums = [1, 2, 3]
>>> a = (ctypes.c_double * len(nums))(*nums)
>>> a
<__main__.c_double_Array_3 object at 0x10069cd40>
>>> a[0]
1.0
>>> a[1]
2.0
>>> a[2]
3.0
>>>
對于數(shù)組對象,from_array()
提取底層的內(nèi)存指針并將其轉(zhuǎn)換為一個 ctypes
指針對象。例如:
>>> import array
>>> a = array.array('d',[1,2,3])
>>> a
array('d', [1.0, 2.0, 3.0])
>>> ptr_ = a.buffer_info()
>>> ptr
4298687200
>>> ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
<__main__.LP_c_double object at 0x10069cd40>
>>>
from_ndarray()
演示了對于 numpy
數(shù)組的轉(zhuǎn)換操作。 通過定義 DoubleArrayType
類并在 avg()
類型簽名中使用它, 那么這個函數(shù)就能接受多個不同的類數(shù)組輸入了:
>>> import sample
>>> sample.avg([1,2,3])
2.0
>>> sample.avg((1,2,3))
2.0
>>> import array
>>> sample.avg(array.array('d',[1,2,3]))
2.0
>>> import numpy
>>> sample.avg(numpy.array([1.0,2.0,3.0]))
2.0
>>>
本節(jié)最后一部分向你演示了怎樣處理一個簡單的 C 結(jié)構(gòu)。 對于結(jié)構(gòu)體,你只需要像下面這樣簡單的定義一個類,包含相應(yīng)的字段和類型即可:
class Point(ctypes.Structure):
_fields_ = [('x', ctypes.c_double),
('y', ctypes.c_double)]
一旦類被定義后,你就可以在類型簽名中或者是需要實例化結(jié)構(gòu)體的代碼中使用它。例如:
>>> p1 = sample.Point(1,2)
>>> p2 = sample.Point(4,5)
>>> p1.x
1.0
>>> p1.y
2.0
>>> sample.distance(p1,p2)
4.242640687119285
>>>
最后一些小的提示:如果你想在 Python 中訪問一些小的 C 函數(shù),那么 ctypes
是一個很有用的函數(shù)庫。 盡管如此,如果你想要去訪問一個很大的庫,那么可能就需要其他的方法了,比如 Swig
(15.9節(jié)會講到) 或 Cython(15.10節(jié))。
對于大型庫的訪問有個主要問題,由于 ctypes 并不是完全自動化, 那么你就必須花費大量時間來編寫所有的類型簽名,就像例子中那樣。 如果函數(shù)庫夠復(fù)雜,你還得去編寫很多小的包裝函數(shù)和支持類。 另外,除非你已經(jīng)完全精通了所有底層的 C 接口細(xì)節(jié),包括內(nèi)存分配和錯誤處理機(jī)制, 通常一個很小的代碼缺陷、訪問越界或其他類似錯誤就能讓 Python 程序奔潰。
作為 ctypes
的一個替代,你還可以考慮下 CFFI。CFFI 提供了很多類似的功能, 但是使用 C 語法并支持更多高級的C代碼類型。 到寫這本書為止,CFFI 還是一個相對較新的工程, 但是它的流行度正在快速上升。 甚至還有在討論在 Python 將來的版本中將它包含進(jìn)去。因此,這個真的值得一看。
你想不依靠其他工具,直接使用 Python 的擴(kuò)展 API 來編寫一些簡單的 C 擴(kuò)展模塊。
對于簡單的 C 代碼,構(gòu)建一個自定義擴(kuò)展模塊是很容易的。 作為第一步,你需要確保你的C代碼有一個正確的頭文件。例如:
/* sample.h */
#include <math.h>
extern int gcd(int, int);
extern int in_mandel(double x0, double y0, int n);
extern int divide(int a, int b, int *remainder);
extern double avg(double *a, int n);
typedef struct Point {
double x,y;
} Point;
extern double distance(Point *p1, Point *p2);
通常來講,這個頭文件要對應(yīng)一個已經(jīng)被單獨編譯過的庫。 有了這些,下面我們演示下編寫擴(kuò)展函數(shù)的一個簡單例子:
#include "Python.h"
#include "sample.h"
/* int gcd(int, int) */
static PyObject *py_gcd(PyObject *self, PyObject *args) {
int x, y, result;
if (!PyArg_ParseTuple(args,"ii", &x, &y)) {
return NULL;
}
result = gcd(x,y);
return Py_BuildValue("i", result);
}
/* int in_mandel(double, double, int) */
static PyObject *py_in_mandel(PyObject *self, PyObject *args) {
double x0, y0;
int n;
int result;
if (!PyArg_ParseTuple(args, "ddi", &x0, &y0, &n)) {
return NULL;
}
result = in_mandel(x0,y0,n);
return Py_BuildValue("i", result);
}
/* int divide(int, int, int *) */
static PyObject *py_divide(PyObject *self, PyObject *args) {
int a, b, quotient, remainder;
if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
return NULL;
}
quotient = divide(a,b, &remainder);
return Py_BuildValue("(ii)", quotient, remainder);
}
/* Module method table */
static PyMethodDef SampleMethods[] = {
{"gcd", py_gcd, METH_VARARGS, "Greatest common divisor"},
{"in_mandel", py_in_mandel, METH_VARARGS, "Mandelbrot test"},
{"divide", py_divide, METH_VARARGS, "Integer division"},
{ NULL, NULL, 0, NULL}
};
/* Module structure */
static struct PyModuleDef samplemodule = {
PyModuleDef_HEAD_INIT,
"sample", /* name of module */
"A sample module", /* Doc string (may be NULL) */
-1, /* Size of per-interpreter state or -1 */
SampleMethods /* Method table */
};
/* Module initialization function */
PyMODINIT_FUNC
PyInit_sample(void) {
return PyModule_Create(&samplemodule);
}
要綁定這個擴(kuò)展模塊,像下面這樣創(chuàng)建一個 setup.py
文件:
# setup.py
from distutils.core import setup, Extension
setup(name='sample',
ext_modules=[
Extension('sample',
['pysample.c'],
include_dirs = ['/some/dir'],
define_macros = [('FOO','1')],
undef_macros = ['BAR'],
library_dirs = ['/usr/local/lib'],
libraries = ['sample']
)
]
)
為了構(gòu)建最終的函數(shù)庫,只需簡單的使用 python3 buildlib.py build_ext --inplace
命令即可:
bash % python3 setup.py build_ext --inplace
running build_ext
building 'sample' extension
gcc -fno-strict-aliasing -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes
-I/usr/local/include/python3.3m -c pysample.c
-o build/temp.macosx-10.6-x86_64-3.3/pysample.o
gcc -bundle -undefined dynamic_lookup
build/temp.macosx-10.6-x86_64-3.3/pysample.o \
-L/usr/local/lib -lsample -o sample.so
bash %
如上所示,它會創(chuàng)建一個名字叫 sample.so
的共享庫。當(dāng)被編譯后,你就能將它作為一個模塊導(dǎo)入進(jìn)來了:
>>> import sample
>>> sample.gcd(35, 42)
7
>>> sample.in_mandel(0, 0, 500)
1
>>> sample.in_mandel(2.0, 1.0, 500)
0
>>> sample.divide(42, 8)
(5, 2)
>>>
如果你是在 Windows 機(jī)器上面嘗試這些步驟,可能會遇到各種環(huán)境和編譯問題,你需要花更多點時間去配置。 Python 的二進(jìn)制分發(fā)通常使用了 Microsoft Visual Studio 來構(gòu)建。 為了讓這些擴(kuò)展能正常工作,你需要使用同樣或兼容的工具來編譯它。 參考相應(yīng)的 Python 文檔
在嘗試任何手寫擴(kuò)展之前,最好能先參考下 Python 文檔中的擴(kuò)展和嵌入 Python 解釋器. Python 的 C 擴(kuò)展 API 很大,在這里整個去講述它沒什么實際意義。 不過對于最核心的部分還是可以討論下的。
首先,在擴(kuò)展模塊中,你寫的函數(shù)都是像下面這樣的一個普通原型:
static PyObject *py_func(PyObject *self, PyObject *args) {
...
}
PyObject
是一個能表示任何 Python 對象的 C 數(shù)據(jù)類型。 在一個高級層面,一個擴(kuò)展函數(shù)就是一個接受一個 Python 對象 (在 PyObject *args 中)元組并返回一個新 Python 對象的 C 函數(shù)。 函數(shù)的 self
參數(shù)對于簡單的擴(kuò)展函數(shù)沒有被使用到, 不過如果你想定義新的類或者是 C 中的對象類型的話就能派上用場了。比如如果擴(kuò)展函數(shù)是一個類的一個方法, 那么 self
就能引用那個實例了。
PyArg_ParseTuple()
函數(shù)被用來將 Python 中的值轉(zhuǎn)換成 C 中對應(yīng)表示。 它接受一個指定輸入格式的格式化字符串作為輸入,比如“i”代表整數(shù),“d”代表雙精度浮點數(shù), 同樣還有存放轉(zhuǎn)換后結(jié)果的 C 變量的地址。 如果輸入的值不匹配這個格式化字符串,就會拋出一個異常并返回一個 NULL 值。 通過檢查并返回 NULL,一個合適的異常會在調(diào)用代碼中被拋出。
Py_BuildValue()
函數(shù)被用來根據(jù) C 數(shù)據(jù)類型創(chuàng)建 Python 對象。 它同樣接受一個格式化字符串來指定期望類型。 在擴(kuò)展函數(shù)中,它被用來返回結(jié)果給 Python。 Py_BuildValue()
的一個特性是它能構(gòu)建更加復(fù)雜的對象類型,比如元組和字典。 在 py_divide()
代碼中,一個例子演示了怎樣返回一個元組。不過,下面還有一些實例:
return Py_BuildValue("i", 34); // Return an integer
return Py_BuildValue("d", 3.4); // Return a double
return Py_BuildValue("s", "Hello"); // Null-terminated UTF-8 string
return Py_BuildValue("(ii)", 3, 4); // Tuple (3, 4)
在擴(kuò)展模塊底部,你會發(fā)現(xiàn)一個函數(shù)表,比如本節(jié)中的 SampleMethods
表。 這個表可以列出 C 函數(shù)、Python 中使用的名字、文檔字符串。 所有模塊都需要指定這個表,因為它在模塊初始化時要被使用到。
最后的函數(shù) PyInit_sample()
是模塊初始化函數(shù),但該模塊第一次被導(dǎo)入時執(zhí)行。 這個函數(shù)的主要工作是在解釋器中注冊模塊對象。
最后一個要點需要提出來,使用 C 函數(shù)來擴(kuò)展 Python 要考慮的事情還有很多,本節(jié)只是一小部分。 (實際上,C API 包含了超過500個函數(shù))。你應(yīng)該將本節(jié)當(dāng)做是一個入門篇。 更多高級內(nèi)容,可以看看 PyArg_ParseTuple()
和 Py_BuildValue()
函數(shù)的文檔, 然后進(jìn)一步擴(kuò)展開。
你想編寫一個 C 擴(kuò)展函數(shù)來操作數(shù)組,可能是被 array 模塊或類似 Numpy 庫所創(chuàng)建。 不過,你想讓你的函數(shù)更加通用,而不是針對某個特定的庫所生成的數(shù)組。
為了能讓接受和處理數(shù)組具有可移植性,你需要使用到 Buffer Protocol . 下面是一個手寫的 C 擴(kuò)展函數(shù)例子, 用來接受數(shù)組數(shù)據(jù)并調(diào)用本章開篇部分的 avg(double *buf, int len)
函數(shù):
/* Call double avg(double *, int) */
static PyObject *py_avg(PyObject *self, PyObject *args) {
PyObject *bufobj;
Py_buffer view;
double result;
/* Get the passed Python object */
if (!PyArg_ParseTuple(args, "O", &bufobj)) {
return NULL;
}
/* Attempt to extract buffer information from it */
if (PyObject_GetBuffer(bufobj, &view,
PyBUF_ANY_CONTIGUOUS | PyBUF_FORMAT) == -1) {
return NULL;
}
if (view.ndim != 1) {
PyErr_SetString(PyExc_TypeError, "Expected a 1-dimensional array");
PyBuffer_Release(&view);
return NULL;
}
/* Check the type of items in the array */
if (strcmp(view.format,"d") != 0) {
PyErr_SetString(PyExc_TypeError, "Expected an array of doubles");
PyBuffer_Release(&view);
return NULL;
}
/* Pass the raw buffer and size to the C function */
result = avg(view.buf, view.shape[0]);
/* Indicate we're done working with the buffer */
PyBuffer_Release(&view);
return Py_BuildValue("d", result);
}
下面我們演示下這個擴(kuò)展函數(shù)是如何工作的:
>>> import array
>>> avg(array.array('d',[1,2,3]))
2.0
>>> import numpy
>>> avg(numpy.array([1.0,2.0,3.0]))
2.0
>>> avg([1,2,3])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'list' does not support the buffer interface
>>> avg(b'Hello')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Expected an array of doubles
>>> a = numpy.array([[1.,2.,3.],[4.,5.,6.]])
>>> avg(a[:,2])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: ndarray is not contiguous
>>> sample.avg(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Expected a 1-dimensional array
>>> sample.avg(a[0])
2.0
>>>
將一個數(shù)組對象傳給 C 函數(shù)可能是一個擴(kuò)展函數(shù)做的最常見的事。 很多 Python 應(yīng)用程序,從圖像處理到科學(xué)計算,都是基于高性能的數(shù)組處理。 通過編寫能接受并操作數(shù)組的代碼,你可以編寫很好的兼容這些應(yīng)用程序的自定義代碼, 而不是只能兼容你自己的代碼。
代碼的關(guān)鍵點在于 PyBuffer_GetBuffer()
函數(shù)。 給定一個任意的 Python 對象,它會試著去獲取底層內(nèi)存信息,它簡單的拋出一個異常并返回-1. 傳給 PyBuffer_GetBuffer()
的特殊標(biāo)志給出了所需的內(nèi)存緩沖類型。 例如,PyBUF_ANY_CONTIGUOUS
表示是一個聯(lián)系的內(nèi)存區(qū)域。
對于數(shù)組、字節(jié)字符串和其他類似對象而言,一個Py_buffer
結(jié)構(gòu)體包含了所有底層內(nèi)存的信息。 它包含一個指向內(nèi)存地址、大小、元素大小、格式和其他細(xì)節(jié)的指針。下面是這個結(jié)構(gòu)體的定義:
typedef struct bufferinfo {
void *buf; /* Pointer to buffer memory */
PyObject *obj; /* Python object that is the owner */
Py_ssize_t len; /* Total size in bytes */
Py_ssize_t itemsize; /* Size in bytes of a single item */
int readonly; /* Read-only access flag */
int ndim; /* Number of dimensions */
char *format; /* struct code of a single item */
Py_ssize_t *shape; /* Array containing dimensions */
Py_ssize_t *strides; /* Array containing strides */
Py_ssize_t *suboffsets; /* Array containing suboffsets */
} Py_buffer;
本節(jié)中,我們只關(guān)注接受一個雙精度浮點數(shù)數(shù)組作為參數(shù)。 要檢查元素是否是一個雙精度浮點數(shù),只需驗證 format
屬性是不是字符串”d”. 這個也是struct
模塊用來編碼二進(jìn)制數(shù)據(jù)的。 通常來講,format
可以是任何兼容 struct
模塊的格式化字符串, 并且如果數(shù)組包含了 C 結(jié)構(gòu)的話它可以包含多個值。 一旦我們已經(jīng)確定了底層的緩存區(qū)信息,那只需要簡單的將它傳給 C 函數(shù),然后會被當(dāng)做是一個普通的 C 數(shù)組了。 實際上,我們不必?fù)?dān)心是怎樣的數(shù)組類型或者它是被什么庫創(chuàng)建出來的。 這也是為什么這個函數(shù)能兼容array
模塊也能兼容numpy
模塊中的數(shù)組了。
在返回最終結(jié)果之前,底層的緩沖區(qū)視圖必須使用 PyBuffer_Release()
釋放掉。 之所以要這一步是為了能正確的管理對象的引用計數(shù)。
同樣,本節(jié)也僅僅只是演示了接受數(shù)組的一個小的代碼片段。 如果你真的要處理數(shù)組,你可能會碰到多維數(shù)據(jù)、大數(shù)據(jù)、不同的數(shù)據(jù)類型等等問題, 那么就得去學(xué)更高級的東西了。你需要參考官方文檔來獲取更多詳細(xì)的細(xì)節(jié)。
如果你需要編寫涉及到數(shù)組處理的多個擴(kuò)展,那么通過 Cython 來實現(xiàn)會更容易下。參考15.11節(jié)。
你有一個擴(kuò)展模塊需要處理 C 結(jié)構(gòu)體中的指針, 但是你又不想暴露結(jié)構(gòu)體中任何內(nèi)部細(xì)節(jié)給 Python。
隱形結(jié)構(gòu)體可以很容易的通過將它們包裝在膠囊對象中來處理。 考慮我們例子代碼中的下列 C 代碼片段:
typedef struct Point {
double x,y;
} Point;
extern double distance(Point *p1, Point *p2);
下面是一個使用膠囊包裝 Point 結(jié)構(gòu)體和 distance()
函數(shù)的擴(kuò)展代碼實例:
/* Destructor function for points */
static void del_Point(PyObject *obj) {
free(PyCapsule_GetPointer(obj,"Point"));
}
/* Utility functions */
static Point *PyPoint_AsPoint(PyObject *obj) {
return (Point *) PyCapsule_GetPointer(obj, "Point");
}
static PyObject *PyPoint_FromPoint(Point *p, int must_free) {
return PyCapsule_New(p, "Point", must_free ? del_Point : NULL);
}
/* Create a new Point object */
static PyObject *py_Point(PyObject *self, PyObject *args) {
Point *p;
double x,y;
if (!PyArg_ParseTuple(args,"dd",&x,&y)) {
return NULL;
}
p = (Point *) malloc(sizeof(Point));
p->x = x;
p->y = y;
return PyPoint_FromPoint(p, 1);
}
static PyObject *py_distance(PyObject *self, PyObject *args) {
Point *p1, *p2;
PyObject *py_p1, *py_p2;
double result;
if (!PyArg_ParseTuple(args,"OO",&py_p1, &py_p2)) {
return NULL;
}
if (!(p1 = PyPoint_AsPoint(py_p1))) {
return NULL;
}
if (!(p2 = PyPoint_AsPoint(py_p2))) {
return NULL;
}
result = distance(p1,p2);
return Py_BuildValue("d", result);
}
在 Python 中可以像下面這樣來使用這些函數(shù):
>>> import sample
>>> p1 = sample.Point(2,3)
>>> p2 = sample.Point(4,5)
>>> p1
<capsule object "Point" at 0x1004ea330>
>>> p2
<capsule object "Point" at 0x1005d1db0>
>>> sample.distance(p1,p2)
2.8284271247461903
>>>
膠囊和 C 指針類似。在內(nèi)部,它們獲取一個通用指針和一個名稱,可以使用 PyCapsule_New()
函數(shù)很容易的被創(chuàng)建。 另外,一個可選的析構(gòu)函數(shù)能被綁定到膠囊上,用來在膠囊對象被垃圾回收時釋放底層的內(nèi)存。
要提取膠囊中的指針,可使用 PyCapsule_GetPointer()
函數(shù)并指定名稱。 如果提供的名稱和膠囊不匹配或其他錯誤出現(xiàn),那么就會拋出異常并返回 NULL。
本節(jié)中,一對工具函數(shù)—— PyPoint_FromPoint()
和PyPoint_AsPoint()
被用來創(chuàng)建和從膠囊對象中提取 Point 實例。 在任何擴(kuò)展函數(shù)中,我們會使用這些函數(shù)而不是直接使用膠囊對象。 這種設(shè)計使得我們可以很容易的應(yīng)對將來對 Point 底下的包裝的更改。 例如,如果你決定使用另外一個膠囊了,那么只需要更改這兩個函數(shù)即可。
對于膠囊對象一個難點在于垃圾回收和內(nèi)存管理。 PyPoint_FromPoint()
函數(shù)接受一個must_free
參數(shù), 用來指定當(dāng)膠囊被銷毀時底層 Point * 結(jié)構(gòu)體是否應(yīng)該被回收。 在某些 C 代碼中,歸屬問題通常很難被處理(比如一個 Point 結(jié)構(gòu)體被嵌入到一個被單獨管理的大結(jié)構(gòu)體中)。 程序員可以使用 extra
參數(shù)來控制,而不是單方面的決定垃圾回收。 要注意的是和現(xiàn)有膠囊有關(guān)的析構(gòu)器能使用 PyCapsule_SetDestructor()
函數(shù)來更改。
對于涉及到結(jié)構(gòu)體的 C 代碼而言,使用膠囊是一個比較合理的解決方案。 例如,有時候你并不關(guān)心暴露結(jié)構(gòu)體的內(nèi)部信息或者將其轉(zhuǎn)換成一個完整的擴(kuò)展類型。 通過使用膠囊,你可以在它上面放一個輕量級的包裝器,然后將它傳給其他的擴(kuò)展函數(shù)。
你有一個 C 擴(kuò)展模塊,在內(nèi)部定義了很多有用的函數(shù),你想將它們導(dǎo)出為一個公共的 C API 供其他地方使用。 你想在其他擴(kuò)展模塊中使用這些函數(shù),但是不知道怎樣將它們鏈接起來, 并且通過 C 編譯器/鏈接器來做看上去特別復(fù)雜(或者不可能做到)。
本節(jié)主要問題是如何處理15.4小節(jié)中提到的 Point 對象。仔細(xì)回一下,在 C 代碼中包含了如下這些工具函數(shù):
/* Destructor function for points */
static void del_Point(PyObject *obj) {
free(PyCapsule_GetPointer(obj,"Point"));
}
/* Utility functions */
static Point *PyPoint_AsPoint(PyObject *obj) {
return (Point *) PyCapsule_GetPointer(obj, "Point");
}
static PyObject *PyPoint_FromPoint(Point *p, int must_free) {
return PyCapsule_New(p, "Point", must_free ? del_Point : NULL);
}
現(xiàn)在的問題是怎樣將 PyPoint_AsPoint()
和 Point_FromPoint()
函數(shù)作為 API 導(dǎo)出, 這樣其他擴(kuò)展模塊能使用并鏈接它們,比如如果你有其他擴(kuò)展也想使用包裝的 Point 對象。
要解決這個問題,首先要為sample
擴(kuò)展寫個新的頭文件名叫 pysample.h
,如下:
/* pysample.h */
#include "Python.h"
#include "sample.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Public API Table */
typedef struct {
Point *(*aspoint)(PyObject *);
PyObject *(*frompoint)(Point *, int);
} _PointAPIMethods;
#ifndef PYSAMPLE_MODULE
/* Method table in external module */
static _PointAPIMethods *_point_api = 0;
/* Import the API table from sample */
static int import_sample(void) {
_point_api = (_PointAPIMethods *) PyCapsule_Import("sample._point_api",0);
return (_point_api != NULL) ? 1 : 0;
}
/* Macros to implement the programming interface */
#define PyPoint_AsPoint(obj) (_point_api->aspoint)(obj)
#define PyPoint_FromPoint(obj) (_point_api->frompoint)(obj)
#endif
#ifdef __cplusplus
}
#endif
這里最重要的部分是函數(shù)指針表 _PointAPIMethods
. 它會在導(dǎo)出模塊時被初始化,然后導(dǎo)入模塊時被查找到。 修改原始的擴(kuò)展模塊來填充表格并將它像下面這樣導(dǎo)出:
/* pysample.c */
#include "Python.h"
#define PYSAMPLE_MODULE
#include "pysample.h"
...
/* Destructor function for points */
static void del_Point(PyObject *obj) {
printf("Deleting point\n");
free(PyCapsule_GetPointer(obj,"Point"));
}
/* Utility functions */
static Point *PyPoint_AsPoint(PyObject *obj) {
return (Point *) PyCapsule_GetPointer(obj, "Point");
}
static PyObject *PyPoint_FromPoint(Point *p, int free) {
return PyCapsule_New(p, "Point", free ? del_Point : NULL);
}
static _PointAPIMethods _point_api = {
PyPoint_AsPoint,
PyPoint_FromPoint
};
...
/* Module initialization function */
PyMODINIT_FUNC
PyInit_sample(void) {
PyObject *m;
PyObject *py_point_api;
m = PyModule_Create(&samplemodule);
if (m == NULL)
return NULL;
/* Add the Point C API functions */
py_point_api = PyCapsule_New((void *) &_point_api, "sample._point_api", NULL);
if (py_point_api) {
PyModule_AddObject(m, "_point_api", py_point_api);
}
return m;
}
最后,下面是一個新的擴(kuò)展模塊例子,用來加載并使用這些 API 函數(shù):
/* ptexample.c */
/* Include the header associated with the other module */
#include "pysample.h"
/* An extension function that uses the exported API */
static PyObject *print_point(PyObject *self, PyObject *args) {
PyObject *obj;
Point *p;
if (!PyArg_ParseTuple(args,"O", &obj)) {
return NULL;
}
/* Note: This is defined in a different module */
p = PyPoint_AsPoint(obj);
if (!p) {
return NULL;
}
printf("%f %f\n", p->x, p->y);
return Py_BuildValue("");
}
static PyMethodDef PtExampleMethods[] = {
{"print_point", print_point, METH_VARARGS, "output a point"},
{ NULL, NULL, 0, NULL}
};
static struct PyModuleDef ptexamplemodule = {
PyModuleDef_HEAD_INIT,
"ptexample", /* name of module */
"A module that imports an API", /* Doc string (may be NULL) */
-1, /* Size of per-interpreter state or -1 */
PtExampleMethods /* Method table */
};
/* Module initialization function */
PyMODINIT_FUNC
PyInit_ptexample(void) {
PyObject *m;
m = PyModule_Create(&ptexamplemodule);
if (m == NULL)
return NULL;
/* Import sample, loading its API functions */
if (!import_sample()) {
return NULL;
}
return m;
}
編譯這個新模塊時,你甚至不需要去考慮怎樣將函數(shù)庫或代碼跟其他模塊鏈接起來。 例如,你可以像下面這樣創(chuàng)建一個簡單的 setup.py
文件:
# setup.py
from distutils.core import setup, Extension
setup(name='ptexample',
ext_modules=[
Extension('ptexample',
['ptexample.c'],
include_dirs = [], # May need pysample.h directory
)
]
)
如果一切正常,你會發(fā)現(xiàn)你的新擴(kuò)展函數(shù)能和定義在其他模塊中的C API函數(shù)一起運(yùn)行的很好。
>>> import sample
>>> p1 = sample.Point(2,3)
>>> p1
<capsule object "Point *" at 0x1004ea330>
>>> import ptexample
>>> ptexample.print_point(p1)
2.000000 3.000000
>>>
本節(jié)基于一個前提就是,膠囊對象能獲取任何你想要的對象的指針。 這樣的話,定義模塊會填充一個函數(shù)指針的結(jié)構(gòu)體,創(chuàng)建一個指向它的膠囊,并在一個模塊級屬性中保存這個膠囊, 例如 sample._point_api
.
其他模塊能夠在導(dǎo)入時獲取到這個屬性并提取底層的指針。 事實上,Python 提供了 PyCapsule_Import()
工具函數(shù),為了完成所有的步驟。 你只需提供屬性的名字即可(比如 sample._point_api),然后他就會一次性找到膠囊對象并提取出指針來。
在將被導(dǎo)出函數(shù)變?yōu)槠渌K中普通函數(shù)時,有一些 C 編程陷阱需要指出來。 在 pysample.h
文件中,一個_point_api
指針被用來指向在導(dǎo)出模塊中被初始化的方法表。 一個相關(guān)的函數(shù) import_sample()
被用來指向膠囊導(dǎo)入并初始化這個指針。 這個函數(shù)必須在任何函數(shù)被使用之前被調(diào)用。通常來講,它會在模塊初始化時被調(diào)用到。 最后,C 的預(yù)處理宏被定義,被用來通過方法表去分發(fā)這些 API 函數(shù)。 用戶只需要使用這些原始函數(shù)名稱即可,不需要通過宏去了解其他信息。
最后,還有一個重要的原因讓你去使用這個技術(shù)來鏈接模塊——它非常簡單并且可以使得各個模塊很清晰的解耦。 如果你不想使用本機(jī)的技術(shù),那你就必須使用共享庫的高級特性和動態(tài)加載器來鏈接模塊。 例如,將一個普通的 API 函數(shù)放入一個共享庫并確保所有擴(kuò)展模塊鏈接到那個共享庫。 這種方法確實可行,但是它相對繁瑣,特別是在大型系統(tǒng)中。 本節(jié)演示了如何通過 Python 的普通導(dǎo)入機(jī)制和僅僅幾個膠囊調(diào)用來將多個模塊鏈接起來的魔法。 對于模塊的編譯,你只需要定義頭文件,而不需要考慮函數(shù)庫的內(nèi)部細(xì)節(jié)。
更多關(guān)于利用 C API 來構(gòu)造擴(kuò)展模塊的信息可以參考 Python 的文檔
你想在 C 中安全的執(zhí)行某個 Python 調(diào)用并返回結(jié)果給 C。 例如,你想在 C 語言中使用某個 Python 函數(shù)作為一個回調(diào)。
在 C 語言中調(diào)用 Python 非常簡單,不過設(shè)計到一些小竅門。 下面的 C 代碼告訴你怎樣安全的調(diào)用:
#include <Python.h>
/* Execute func(x,y) in the Python interpreter. The
arguments and return result of the function must
be Python floats */
double call_func(PyObject *func, double x, double y) {
PyObject *args;
PyObject *kwargs;
PyObject *result = 0;
double retval;
/* Make sure we own the GIL */
PyGILState_STATE state = PyGILState_Ensure();
/* Verify that func is a proper callable */
if (!PyCallable_Check(func)) {
fprintf(stderr,"call_func: expected a callable\n");
goto fail;
}
/* Build arguments */
args = Py_BuildValue("(dd)", x, y);
kwargs = NULL;
/* Call the function */
result = PyObject_Call(func, args, kwargs);
Py_DECREF(args);
Py_XDECREF(kwargs);
/* Check for Python exceptions (if any) */
if (PyErr_Occurred()) {
PyErr_Print();
goto fail;
}
/* Verify the result is a float object */
if (!PyFloat_Check(result)) {
fprintf(stderr,"call_func: callable didn't return a float\n");
goto fail;
}
/* Create the return value */
retval = PyFloat_AsDouble(result);
Py_DECREF(result);
/* Restore previous GIL state and return */
PyGILState_Release(state);
return retval;
fail:
Py_XDECREF(result);
PyGILState_Release(state);
abort(); // Change to something more appropriate
}
要使用這個函數(shù),你需要獲取傳遞過來的某個已存在 Python 調(diào)用的引用。 有很多種方法可以讓你這樣做, 比如將一個可調(diào)用對象傳給一個擴(kuò)展模塊或直接寫 C 代碼從已存在模塊中提取出來。
下面是一個簡單例子用來掩飾從一個嵌入的 Python 解釋器中調(diào)用一個函數(shù):
#include <Python.h>
/* Definition of call_func() same as above */
...
/* Load a symbol from a module */
PyObject *import_name(const char *modname, const char *symbol) {
PyObject *u_name, *module;
u_name = PyUnicode_FromString(modname);
module = PyImport_Import(u_name);
Py_DECREF(u_name);
return PyObject_GetAttrString(module, symbol);
}
/* Simple embedding example */
int main() {
PyObject *pow_func;
double x;
Py_Initialize();
/* Get a reference to the math.pow function */
pow_func = import_name("math","pow");
/* Call it using our call_func() code */
for (x = 0.0; x < 10.0; x += 0.1) {
printf("%0.2f %0.2f\n", x, call_func(pow_func,x,2.0));
}
/* Done */
Py_DECREF(pow_func);
Py_Finalize();
return 0;
}
要構(gòu)建例子代碼,你需要編譯 C 并將它鏈接到 Python 解釋器。 下面的 Makefile 可以教你怎樣做(不過在你機(jī)器上面需要一些配置)。
all::
cc -g embed.c -I/usr/local/include/python3.3m \
-L/usr/local/lib/python3.3/config-3.3m -lpython3.3m
編譯并運(yùn)行會產(chǎn)生類似下面的輸出:
0.00 0.00
0.10 0.01
0.20 0.04
0.30 0.09
0.40 0.16
...
下面是一個稍微不同的例子,展示了一個擴(kuò)展函數(shù), 它接受一個可調(diào)用對象和其他參數(shù),并將它們傳遞給 call_func()
來做測試:
/* Extension function for testing the C-Python callback */
PyObject *py_call_func(PyObject *self, PyObject *args) {
PyObject *func;
double x, y, result;
if (!PyArg_ParseTuple(args,"Odd", &func,&x,&y)) {
return NULL;
}
result = call_func(func, x, y);
return Py_BuildValue("d", result);
}
使用這個擴(kuò)展函數(shù),你要像下面這樣測試它:
>>> import sample
>>> def add(x,y):
... return x+y
...
>>> sample.call_func(add,3,4)
7.0
>>>
如果你在 C 語言中調(diào)用 Python,要記住最重要的是 C 語言會是主體。 也就是說,C 語言負(fù)責(zé)構(gòu)造參數(shù)、調(diào)用 Python 函數(shù)、檢查異常、檢查類型、提取返回值等。
作為第一步,你必須先有一個表示你將要調(diào)用的 Python 可調(diào)用對象。 這可以是一個函數(shù)、類、方法、內(nèi)置方法或其他任意實現(xiàn)了 __call__()
操作的東西。 為了確保是可調(diào)用的,可以像下面的代碼這樣利用PyCallable_Check()
做檢查:
double call_func(PyObject *func, double x, double y) {
...
/* Verify that func is a proper callable */
if (!PyCallable_Check(func)) {
fprintf(stderr,"call_func: expected a callable\n");
goto fail;
}
...
在 C 代碼里處理錯誤你需要格外的小心。一般來講,你不能僅僅拋出一個 Python 異常。 錯誤應(yīng)該使用 C 代碼方式來被處理。在這里,我們打算將對錯誤的控制傳給一個叫 abort()
的錯誤處理器。 它會結(jié)束掉整個程序,在真實環(huán)境下面你應(yīng)該要處理的更加優(yōu)雅些(返回一個狀態(tài)碼)。 你要記住的是在這里 C 是主角,因此并沒有跟拋出異常相對應(yīng)的操作。 錯誤處理是你在編程時必須要考慮的事情。
調(diào)用一個函數(shù)相對來講很簡單——只需要使用 PyObject_Call()
, 傳一個可調(diào)用對象給它、一個參數(shù)元組和一個可選的關(guān)鍵字字典。 要構(gòu)建參數(shù)元組或字典,你可以使用 Py_BuildValue()
,如下:
double call_func(PyObject *func, double x, double y) {
PyObject *args;
PyObj