鍍金池/ 教程/ Python/ C 語(yǔ)言擴(kuò)展
類與對(duì)象
模塊與包
數(shù)據(jù)編碼和處理
元編程
網(wǎng)絡(luò)與 Web 編程
數(shù)字日期和時(shí)間
測(cè)試、調(diào)試和異常
字符串和文本
文件與 IO
腳本編程與系統(tǒng)管理
迭代器與生成器
函數(shù)
C 語(yǔ)言擴(kuò)展
并發(fā)編程
數(shù)據(jù)結(jié)構(gòu)和算法

C 語(yǔ)言擴(kuò)展

本章著眼于從 Python 訪問(wèn) C 代碼的問(wèn)題。許多 Python 內(nèi)置庫(kù)是用 C 寫的, 訪問(wèn) C 是讓 Python 的對(duì)現(xiàn)有庫(kù)進(jìn)行交互一個(gè)重要的組成部分。 這也是一個(gè)當(dāng)你面臨從 Python 2 到 Python 3 擴(kuò)展代碼的問(wèn)題。 雖然 Python 提供了一個(gè)廣泛的編程 API,實(shí)際上有很多方法來(lái)處理 C 的代碼。 相比試圖給出對(duì)于每一個(gè)可能的工具或技術(shù)的詳細(xì)參考, 我么采用的是是集中在一個(gè)小片段的 C++ 代碼,以及一些有代表性的例子來(lái)展示如何與代碼交互。 這個(gè)目標(biāo)是提供一系列的編程模板,有經(jīng)驗(yàn)的程序員可以擴(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 語(yǔ)言編程特性。 首先,這里有很多函數(shù)比如 gcd()is_mandel()。 divide() 函數(shù)是一個(gè)返回多個(gè)值的 C 函數(shù)例子,其中有一個(gè)是通過(guò)指針參數(shù)的方式。 avg() 函數(shù)通過(guò)一個(gè) C 數(shù)組執(zhí)行數(shù)據(jù)聚集操作。Pointdistance() 函數(shù)涉及到了 C 結(jié)構(gòu)體。

對(duì)于接下來(lái)的所有小節(jié),先假定上面的代碼已經(jīng)被寫入了一個(gè)名叫“sample.c”的文件中, 然后它們的定義被寫入一個(gè)名叫“sample.h”的頭文件中, 并且被編譯為一個(gè)庫(kù)叫“l(fā)ibsample”,能被鏈接到其他 C 語(yǔ)言代碼中。 編譯和鏈接的細(xì)節(jié)依據(jù)系統(tǒng)的不同而不同,但是這個(gè)不是我們關(guān)注的。 如果你要處理 C 代碼,我們假定這些基礎(chǔ)的東西你都掌握了。

使用 ctypes 訪問(wèn) C 代碼

問(wèn)題

你有一些 C 函數(shù)已經(jīng)被編譯到共享庫(kù)或 DLL 中。你希望可以使用純 Python 代碼調(diào)用這些函數(shù), 而不用編寫額外的 C 代碼或使用第三方擴(kuò)展工具。

解決方案

對(duì)于需要調(diào)用 C 代碼的一些小的問(wèn)題,通常使用 Python 標(biāo)準(zhǔn)庫(kù)中的 ctypes 模塊就足夠了。 要使用ctypes ,你首先要確保你要訪問(wèn)的 C 代碼已經(jīng)被編譯到和 Python 解釋器兼容 (同樣的架構(gòu)、字大小、編譯器等)的某個(gè)共享庫(kù)中了。 為了進(jìn)行本節(jié)的演示,假設(shè)你有一個(gè)共享庫(kù)名字叫 libsample.so,里面的內(nèi)容就是15章介紹部分那樣。 另外還假設(shè)這個(gè) libsample.so 文件被放置到位于sample.py 文件相同的目錄中了。

要訪問(wèn)這個(gè)函數(shù)庫(kù),你要先構(gòu)建一個(gè)包裝它的 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é)有很多值得我們?cè)敿?xì)討論的地方。 首先是對(duì)于 C 和 Python 代碼一起打包的問(wèn)題,如果你在使用 ctypes 來(lái)訪問(wèn)編譯后的C代碼, 那么需要確保這個(gè)共享庫(kù)放在 sample.py模塊同一個(gè)地方。 一種可能是將生成的 .so 文件放置在要使用它的 Python 代碼同一個(gè)目錄下。 我們?cè)?recipe—sample.py 中使用 __file__變量來(lái)查看它被安裝的位置, 然后構(gòu)造一個(gè)指向同一個(gè)目錄中的 libsample.so 文件的路徑。

如果 C 函數(shù)庫(kù)被安裝到其他地方,那么你就要修改相應(yīng)的路徑。 如果 C 函數(shù)庫(kù)在你機(jī)器上被安裝為一個(gè)標(biāo)準(zhǔn)庫(kù)了, 那么可以使用ctypes.util.find_library() 函數(shù)來(lái)查找:

>>> 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ù)庫(kù)的位置,那么就可以像下面這樣使用 ctypes.cdll.LoadLibrary() 來(lái)加載它, 其中 _path是標(biāo)準(zhǔn)庫(kù)的全路徑:

_mod = ctypes.cdll.LoadLibrary(_path)

函數(shù)庫(kù)被加載后,你需要編寫幾個(gè)語(yǔ)句來(lái)提取特定的符號(hào)并指定它們的類型。 就像下面這個(gè)代碼片段一樣:

# 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 屬性是一個(gè)元組,包含了某個(gè)函數(shù)的輸入按時(shí), 而 .restype就是相應(yīng)的返回類型。ctypes 定義了大量的類型對(duì)象(比如 c_double, c_int, c_short, c_float 等), 代表了對(duì)應(yīng)的 C 數(shù)據(jù)類型。如果你想讓 Python 能夠傳遞正確的參數(shù)類型并且正確的轉(zhuǎn)換數(shù)據(jù)的話, 那么這些類型簽名的綁定是很重要的一步。如果你沒(méi)有這么做,不但代碼不能正常運(yùn)行, 還可能會(huì)導(dǎo)致整個(gè)解釋器進(jìn)程掛掉。 使用 ctypes 有一個(gè)麻煩點(diǎn)的地方是原生的 C 代碼使用的術(shù)語(yǔ)可能跟 Python 不能明確的對(duì)應(yīng)上來(lái)。 divide()函數(shù)是一個(gè)很好的例子,它通過(guò)一個(gè)參數(shù)除以另一個(gè)參數(shù)返回一個(gè)結(jié)果值。 盡管這是一個(gè)很常見(jiàn)的 C 技術(shù),但是在 Python 中卻不知道怎樣清晰的表達(dá)出來(lái)。 例如,你不能像下面這樣簡(jiǎn)單的做:

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

就算這個(gè)能正確的工作,它會(huì)違反 Python 對(duì)于整數(shù)的不可更改原則,并且可能會(huì)導(dǎo)致整個(gè)解釋器陷入一個(gè)黑洞中。 對(duì)于涉及到指針的參數(shù),你通常需要先構(gòu)建一個(gè)相應(yīng)的 ctypes 對(duì)象并像下面這樣傳進(jìn)去:

>>> x = ctypes.c_int()
>>> divide(10, 3, x)
3
>>> x.value
1
>>>

在這里,一個(gè) ctypes.c_int實(shí)例被創(chuàng)建并作為一個(gè)指針被傳進(jìn)去。 跟普通 Python 整形不同的是,一個(gè)c_int對(duì)象是可以被修改的。.value屬性可被用來(lái)獲取或更改這個(gè)值。

對(duì)于那些不像 Python 的 C 調(diào)用,通??梢詫懸粋€(gè)小的包裝函數(shù)。 這里,我們讓 divide()函數(shù)通過(guò)元組來(lái)返回兩個(gè)結(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ù)又是一個(gè)新的挑戰(zhàn)。C 代碼期望接受到一個(gè)指針和一個(gè)數(shù)組的長(zhǎng)度值。 但是,在 Python 中,我們必須考慮這個(gè)問(wèn)題:數(shù)組是啥?它是一個(gè)列表?一個(gè)元組? 還是 array模塊中的一個(gè)數(shù)組?還是一個(gè) numpy 數(shù)組?還是說(shuō)所有都是? 實(shí)際上,一個(gè) Python “數(shù)組”有多種形式,你可能想要支持多種可能性。

DoubleArrayType演示了怎樣處理這種情況。 在這個(gè)類中定義了一個(gè)單個(gè)方法 from_param() 。 這個(gè)方法的角色是接受一個(gè)單個(gè)參數(shù)然后將其向下轉(zhuǎn)換為一個(gè)合適的 ctypes 對(duì)象 (本例中是一個(gè) ctypes.c_double 的指針)。 在 from_param()中,你可以做任何你想做的事。 參數(shù)的類型名被提取出來(lái)并被用于分發(fā)到一個(gè)更具體的方法中去。 例如,如果一個(gè)列表被傳遞過(guò)來(lái),那么typename 就是 list, 然后 from_list 方法被調(diào)用。

對(duì)于列表和元組,from_list方法將其轉(zhuǎn)換為一個(gè) ctypes的數(shù)組對(duì)象。 這個(gè)看上去有點(diǎn)奇怪,下面我們使用一個(gè)交互式例子來(lái)將一個(gè)列表轉(zhuǎn)換為一個(gè) 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
>>>

對(duì)于數(shù)組對(duì)象,from_array() 提取底層的內(nèi)存指針并將其轉(zhuǎn)換為一個(gè) ctypes 指針對(duì)象。例如:

>>> 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() 演示了對(duì)于 numpy 數(shù)組的轉(zhuǎn)換操作。 通過(guò)定義 DoubleArrayType類并在 avg() 類型簽名中使用它, 那么這個(gè)函數(shù)就能接受多個(gè)不同的類數(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é)最后一部分向你演示了怎樣處理一個(gè)簡(jiǎn)單的 C 結(jié)構(gòu)。 對(duì)于結(jié)構(gòu)體,你只需要像下面這樣簡(jiǎn)單的定義一個(gè)類,包含相應(yīng)的字段和類型即可:

class Point(ctypes.Structure):
    _fields_ = [('x', ctypes.c_double),
                ('y', ctypes.c_double)]

一旦類被定義后,你就可以在類型簽名中或者是需要實(shí)例化結(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 中訪問(wèn)一些小的 C 函數(shù),那么 ctypes 是一個(gè)很有用的函數(shù)庫(kù)。 盡管如此,如果你想要去訪問(wèn)一個(gè)很大的庫(kù),那么可能就需要其他的方法了,比如 Swig (15.9節(jié)會(huì)講到) 或 Cython(15.10節(jié))。

對(duì)于大型庫(kù)的訪問(wèn)有個(gè)主要問(wèn)題,由于 ctypes 并不是完全自動(dòng)化, 那么你就必須花費(fèi)大量時(shí)間來(lái)編寫所有的類型簽名,就像例子中那樣。 如果函數(shù)庫(kù)夠復(fù)雜,你還得去編寫很多小的包裝函數(shù)和支持類。 另外,除非你已經(jīng)完全精通了所有底層的 C 接口細(xì)節(jié),包括內(nèi)存分配和錯(cuò)誤處理機(jī)制, 通常一個(gè)很小的代碼缺陷、訪問(wèn)越界或其他類似錯(cuò)誤就能讓 Python 程序奔潰。

作為 ctypes 的一個(gè)替代,你還可以考慮下 CFFI。CFFI 提供了很多類似的功能, 但是使用 C 語(yǔ)法并支持更多高級(jí)的C代碼類型。 到寫這本書(shū)為止,CFFI 還是一個(gè)相對(duì)較新的工程, 但是它的流行度正在快速上升。 甚至還有在討論在 Python 將來(lái)的版本中將它包含進(jìn)去。因此,這個(gè)真的值得一看。

簡(jiǎn)單的 C 擴(kuò)展模塊

問(wèn)題

你想不依靠其他工具,直接使用 Python 的擴(kuò)展 API 來(lái)編寫一些簡(jiǎn)單的 C 擴(kuò)展模塊。

解決方案

對(duì)于簡(jiǎn)單的 C 代碼,構(gòu)建一個(gè)自定義擴(kuò)展模塊是很容易的。 作為第一步,你需要確保你的C代碼有一個(gè)正確的頭文件。例如:

/* 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);

通常來(lái)講,這個(gè)頭文件要對(duì)應(yīng)一個(gè)已經(jīng)被單獨(dú)編譯過(guò)的庫(kù)。 有了這些,下面我們演示下編寫擴(kuò)展函數(shù)的一個(gè)簡(jiǎn)單例子:

#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);
}

要綁定這個(gè)擴(kuò)展模塊,像下面這樣創(chuàng)建一個(gè) 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ù)庫(kù),只需簡(jiǎn)單的使用 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 %

如上所示,它會(huì)創(chuàng)建一個(gè)名字叫 sample.so的共享庫(kù)。當(dāng)被編譯后,你就能將它作為一個(gè)模塊導(dǎo)入進(jìn)來(lái)了:

>>> 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ì)遇到各種環(huán)境和編譯問(wèn)題,你需要花更多點(diǎn)時(shí)間去配置。 Python 的二進(jìn)制分發(fā)通常使用了 Microsoft Visual Studio 來(lái)構(gòu)建。 為了讓這些擴(kuò)展能正常工作,你需要使用同樣或兼容的工具來(lái)編譯它。 參考相應(yīng)的 Python 文檔

討論

在嘗試任何手寫擴(kuò)展之前,最好能先參考下 Python 文檔中的擴(kuò)展和嵌入 Python 解釋器. Python 的 C 擴(kuò)展 API 很大,在這里整個(gè)去講述它沒(méi)什么實(shí)際意義。 不過(guò)對(duì)于最核心的部分還是可以討論下的。

首先,在擴(kuò)展模塊中,你寫的函數(shù)都是像下面這樣的一個(gè)普通原型:

static PyObject *py_func(PyObject *self, PyObject *args) {
  ...
}

PyObject 是一個(gè)能表示任何 Python 對(duì)象的 C 數(shù)據(jù)類型。 在一個(gè)高級(jí)層面,一個(gè)擴(kuò)展函數(shù)就是一個(gè)接受一個(gè) Python 對(duì)象 (在 PyObject *args 中)元組并返回一個(gè)新 Python 對(duì)象的 C 函數(shù)。 函數(shù)的 self 參數(shù)對(duì)于簡(jiǎn)單的擴(kuò)展函數(shù)沒(méi)有被使用到, 不過(guò)如果你想定義新的類或者是 C 中的對(duì)象類型的話就能派上用場(chǎng)了。比如如果擴(kuò)展函數(shù)是一個(gè)類的一個(gè)方法, 那么 self 就能引用那個(gè)實(shí)例了。

PyArg_ParseTuple()函數(shù)被用來(lái)將 Python 中的值轉(zhuǎn)換成 C 中對(duì)應(yīng)表示。 它接受一個(gè)指定輸入格式的格式化字符串作為輸入,比如“i”代表整數(shù),“d”代表雙精度浮點(diǎn)數(shù), 同樣還有存放轉(zhuǎn)換后結(jié)果的 C 變量的地址。 如果輸入的值不匹配這個(gè)格式化字符串,就會(huì)拋出一個(gè)異常并返回一個(gè) NULL 值。 通過(guò)檢查并返回 NULL,一個(gè)合適的異常會(huì)在調(diào)用代碼中被拋出。

Py_BuildValue()函數(shù)被用來(lái)根據(jù) C 數(shù)據(jù)類型創(chuàng)建 Python 對(duì)象。 它同樣接受一個(gè)格式化字符串來(lái)指定期望類型。 在擴(kuò)展函數(shù)中,它被用來(lái)返回結(jié)果給 Python。 Py_BuildValue() 的一個(gè)特性是它能構(gòu)建更加復(fù)雜的對(duì)象類型,比如元組和字典。 在 py_divide()代碼中,一個(gè)例子演示了怎樣返回一個(gè)元組。不過(guò),下面還有一些實(shí)例:

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ò)展模塊底部,你會(huì)發(fā)現(xiàn)一個(gè)函數(shù)表,比如本節(jié)中的 SampleMethods表。 這個(gè)表可以列出 C 函數(shù)、Python 中使用的名字、文檔字符串。 所有模塊都需要指定這個(gè)表,因?yàn)樗谀K初始化時(shí)要被使用到。

最后的函數(shù) PyInit_sample() 是模塊初始化函數(shù),但該模塊第一次被導(dǎo)入時(shí)執(zhí)行。 這個(gè)函數(shù)的主要工作是在解釋器中注冊(cè)模塊對(duì)象。

最后一個(gè)要點(diǎn)需要提出來(lái),使用 C 函數(shù)來(lái)擴(kuò)展 Python 要考慮的事情還有很多,本節(jié)只是一小部分。 (實(shí)際上,C API 包含了超過(guò)500個(gè)函數(shù))。你應(yīng)該將本節(jié)當(dāng)做是一個(gè)入門篇。 更多高級(jí)內(nèi)容,可以看看 PyArg_ParseTuple()Py_BuildValue() 函數(shù)的文檔, 然后進(jìn)一步擴(kuò)展開(kāi)。

編寫擴(kuò)展函數(shù)操作數(shù)組

問(wèn)題

你想編寫一個(gè) C 擴(kuò)展函數(shù)來(lái)操作數(shù)組,可能是被 array 模塊或類似 Numpy 庫(kù)所創(chuàng)建。 不過(guò),你想讓你的函數(shù)更加通用,而不是針對(duì)某個(gè)特定的庫(kù)所生成的數(shù)組。

解決方案

為了能讓接受和處理數(shù)組具有可移植性,你需要使用到 Buffer Protocol . 下面是一個(gè)手寫的 C 擴(kuò)展函數(shù)例子, 用來(lái)接受數(shù)組數(shù)據(jù)并調(diào)用本章開(kāi)篇部分的 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);
}

下面我們演示下這個(gè)擴(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
>>>

討論

將一個(gè)數(shù)組對(duì)象傳給 C 函數(shù)可能是一個(gè)擴(kuò)展函數(shù)做的最常見(jiàn)的事。 很多 Python 應(yīng)用程序,從圖像處理到科學(xué)計(jì)算,都是基于高性能的數(shù)組處理。 通過(guò)編寫能接受并操作數(shù)組的代碼,你可以編寫很好的兼容這些應(yīng)用程序的自定義代碼, 而不是只能兼容你自己的代碼。

代碼的關(guān)鍵點(diǎn)在于 PyBuffer_GetBuffer() 函數(shù)。 給定一個(gè)任意的 Python 對(duì)象,它會(huì)試著去獲取底層內(nèi)存信息,它簡(jiǎn)單的拋出一個(gè)異常并返回-1. 傳給 PyBuffer_GetBuffer() 的特殊標(biāo)志給出了所需的內(nèi)存緩沖類型。 例如,PyBUF_ANY_CONTIGUOUS 表示是一個(gè)聯(lián)系的內(nèi)存區(qū)域。

對(duì)于數(shù)組、字節(jié)字符串和其他類似對(duì)象而言,一個(gè)Py_buffer 結(jié)構(gòu)體包含了所有底層內(nèi)存的信息。 它包含一個(gè)指向內(nèi)存地址、大小、元素大小、格式和其他細(xì)節(jié)的指針。下面是這個(gè)結(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)注接受一個(gè)雙精度浮點(diǎn)數(shù)數(shù)組作為參數(shù)。 要檢查元素是否是一個(gè)雙精度浮點(diǎn)數(shù),只需驗(yàn)證 format 屬性是不是字符串”d”. 這個(gè)也是struct 模塊用來(lái)編碼二進(jìn)制數(shù)據(jù)的。 通常來(lái)講,format可以是任何兼容 struct 模塊的格式化字符串, 并且如果數(shù)組包含了 C 結(jié)構(gòu)的話它可以包含多個(gè)值。 一旦我們已經(jīng)確定了底層的緩存區(qū)信息,那只需要簡(jiǎn)單的將它傳給 C 函數(shù),然后會(huì)被當(dāng)做是一個(gè)普通的 C 數(shù)組了。 實(shí)際上,我們不必?fù)?dān)心是怎樣的數(shù)組類型或者它是被什么庫(kù)創(chuàng)建出來(lái)的。 這也是為什么這個(gè)函數(shù)能兼容array模塊也能兼容numpy 模塊中的數(shù)組了。

在返回最終結(jié)果之前,底層的緩沖區(qū)視圖必須使用 PyBuffer_Release()釋放掉。 之所以要這一步是為了能正確的管理對(duì)象的引用計(jì)數(shù)。

同樣,本節(jié)也僅僅只是演示了接受數(shù)組的一個(gè)小的代碼片段。 如果你真的要處理數(shù)組,你可能會(huì)碰到多維數(shù)據(jù)、大數(shù)據(jù)、不同的數(shù)據(jù)類型等等問(wèn)題, 那么就得去學(xué)更高級(jí)的東西了。你需要參考官方文檔來(lái)獲取更多詳細(xì)的細(xì)節(jié)。

如果你需要編寫涉及到數(shù)組處理的多個(gè)擴(kuò)展,那么通過(guò) Cython 來(lái)實(shí)現(xiàn)會(huì)更容易下。參考15.11節(jié)。

在 C 擴(kuò)展模塊中操作隱形指針

問(wèn)題

你有一個(gè)擴(kuò)展模塊需要處理 C 結(jié)構(gòu)體中的指針, 但是你又不想暴露結(jié)構(gòu)體中任何內(nèi)部細(xì)節(jié)給 Python。

解決方案

隱形結(jié)構(gòu)體可以很容易的通過(guò)將它們包裝在膠囊對(duì)象中來(lái)處理。 考慮我們例子代碼中的下列 C 代碼片段:

typedef struct Point {
    double x,y;
} Point;

extern double distance(Point *p1, Point *p2);

下面是一個(gè)使用膠囊包裝 Point 結(jié)構(gòu)體和 distance()函數(shù)的擴(kuò)展代碼實(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);
}

/* 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 中可以像下面這樣來(lái)使用這些函數(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)部,它們獲取一個(gè)通用指針和一個(gè)名稱,可以使用 PyCapsule_New() 函數(shù)很容易的被創(chuàng)建。 另外,一個(gè)可選的析構(gòu)函數(shù)能被綁定到膠囊上,用來(lái)在膠囊對(duì)象被垃圾回收時(shí)釋放底層的內(nèi)存。

要提取膠囊中的指針,可使用 PyCapsule_GetPointer() 函數(shù)并指定名稱。 如果提供的名稱和膠囊不匹配或其他錯(cuò)誤出現(xiàn),那么就會(huì)拋出異常并返回 NULL。

本節(jié)中,一對(duì)工具函數(shù)—— PyPoint_FromPoint()PyPoint_AsPoint() 被用來(lái)創(chuàng)建和從膠囊對(duì)象中提取 Point 實(shí)例。 在任何擴(kuò)展函數(shù)中,我們會(huì)使用這些函數(shù)而不是直接使用膠囊對(duì)象。 這種設(shè)計(jì)使得我們可以很容易的應(yīng)對(duì)將來(lái)對(duì) Point 底下的包裝的更改。 例如,如果你決定使用另外一個(gè)膠囊了,那么只需要更改這兩個(gè)函數(shù)即可。

對(duì)于膠囊對(duì)象一個(gè)難點(diǎn)在于垃圾回收和內(nèi)存管理。 PyPoint_FromPoint() 函數(shù)接受一個(gè)must_free參數(shù), 用來(lái)指定當(dāng)膠囊被銷毀時(shí)底層 Point * 結(jié)構(gòu)體是否應(yīng)該被回收。 在某些 C 代碼中,歸屬問(wèn)題通常很難被處理(比如一個(gè) Point 結(jié)構(gòu)體被嵌入到一個(gè)被單獨(dú)管理的大結(jié)構(gòu)體中)。 程序員可以使用 extra 參數(shù)來(lái)控制,而不是單方面的決定垃圾回收。 要注意的是和現(xiàn)有膠囊有關(guān)的析構(gòu)器能使用 PyCapsule_SetDestructor()函數(shù)來(lái)更改。

對(duì)于涉及到結(jié)構(gòu)體的 C 代碼而言,使用膠囊是一個(gè)比較合理的解決方案。 例如,有時(shí)候你并不關(guān)心暴露結(jié)構(gòu)體的內(nèi)部信息或者將其轉(zhuǎn)換成一個(gè)完整的擴(kuò)展類型。 通過(guò)使用膠囊,你可以在它上面放一個(gè)輕量級(jí)的包裝器,然后將它傳給其他的擴(kuò)展函數(shù)。

從擴(kuò)張模塊中定義和導(dǎo)出 C 的 API

問(wèn)題

你有一個(gè) C 擴(kuò)展模塊,在內(nèi)部定義了很多有用的函數(shù),你想將它們導(dǎo)出為一個(gè)公共的 C API 供其他地方使用。 你想在其他擴(kuò)展模塊中使用這些函數(shù),但是不知道怎樣將它們鏈接起來(lái), 并且通過(guò) C 編譯器/鏈接器來(lái)做看上去特別復(fù)雜(或者不可能做到)。

解決方案

本節(jié)主要問(wèn)題是如何處理15.4小節(jié)中提到的 Point 對(duì)象。仔細(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)在的問(wèn)題是怎樣將 PyPoint_AsPoint()Point_FromPoint()函數(shù)作為 API 導(dǎo)出, 這樣其他擴(kuò)展模塊能使用并鏈接它們,比如如果你有其他擴(kuò)展也想使用包裝的 Point 對(duì)象。

要解決這個(gè)問(wèn)題,首先要為sample 擴(kuò)展寫個(gè)新的頭文件名叫 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. 它會(huì)在導(dǎo)出模塊時(shí)被初始化,然后導(dǎo)入模塊時(shí)被查找到。 修改原始的擴(kuò)展模塊來(lái)填充表格并將它像下面這樣導(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;
}

最后,下面是一個(gè)新的擴(kuò)展模塊例子,用來(lái)加載并使用這些 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;
}

編譯這個(gè)新模塊時(shí),你甚至不需要去考慮怎樣將函數(shù)庫(kù)或代碼跟其他模塊鏈接起來(lái)。 例如,你可以像下面這樣創(chuàng)建一個(gè)簡(jiǎn)單的 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
                  )
        ]
)

如果一切正常,你會(huì)發(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é)基于一個(gè)前提就是,膠囊對(duì)象能獲取任何你想要的對(duì)象的指針。 這樣的話,定義模塊會(huì)填充一個(gè)函數(shù)指針的結(jié)構(gòu)體,創(chuàng)建一個(gè)指向它的膠囊,并在一個(gè)模塊級(jí)屬性中保存這個(gè)膠囊, 例如 sample._point_api .

其他模塊能夠在導(dǎo)入時(shí)獲取到這個(gè)屬性并提取底層的指針。 事實(shí)上,Python 提供了 PyCapsule_Import()工具函數(shù),為了完成所有的步驟。 你只需提供屬性的名字即可(比如 sample._point_api),然后他就會(huì)一次性找到膠囊對(duì)象并提取出指針來(lái)。

在將被導(dǎo)出函數(shù)變?yōu)槠渌K中普通函數(shù)時(shí),有一些 C 編程陷阱需要指出來(lái)。 在 pysample.h 文件中,一個(gè)_point_api 指針被用來(lái)指向在導(dǎo)出模塊中被初始化的方法表。 一個(gè)相關(guān)的函數(shù) import_sample() 被用來(lái)指向膠囊導(dǎo)入并初始化這個(gè)指針。 這個(gè)函數(shù)必須在任何函數(shù)被使用之前被調(diào)用。通常來(lái)講,它會(huì)在模塊初始化時(shí)被調(diào)用到。 最后,C 的預(yù)處理宏被定義,被用來(lái)通過(guò)方法表去分發(fā)這些 API 函數(shù)。 用戶只需要使用這些原始函數(shù)名稱即可,不需要通過(guò)宏去了解其他信息。

最后,還有一個(gè)重要的原因讓你去使用這個(gè)技術(shù)來(lái)鏈接模塊——它非常簡(jiǎn)單并且可以使得各個(gè)模塊很清晰的解耦。 如果你不想使用本機(jī)的技術(shù),那你就必須使用共享庫(kù)的高級(jí)特性和動(dòng)態(tài)加載器來(lái)鏈接模塊。 例如,將一個(gè)普通的 API 函數(shù)放入一個(gè)共享庫(kù)并確保所有擴(kuò)展模塊鏈接到那個(gè)共享庫(kù)。 這種方法確實(shí)可行,但是它相對(duì)繁瑣,特別是在大型系統(tǒng)中。 本節(jié)演示了如何通過(guò) Python 的普通導(dǎo)入機(jī)制和僅僅幾個(gè)膠囊調(diào)用來(lái)將多個(gè)模塊鏈接起來(lái)的魔法。 對(duì)于模塊的編譯,你只需要定義頭文件,而不需要考慮函數(shù)庫(kù)的內(nèi)部細(xì)節(jié)。

更多關(guān)于利用 C API 來(lái)構(gòu)造擴(kuò)展模塊的信息可以參考 Python 的文檔

從 C 語(yǔ)言中調(diào)用 Python 代碼

問(wèn)題

你想在 C 中安全的執(zhí)行某個(gè) Python 調(diào)用并返回結(jié)果給 C。 例如,你想在 C 語(yǔ)言中使用某個(gè) Python 函數(shù)作為一個(gè)回調(diào)。

解決方案

在 C 語(yǔ)言中調(diào)用 Python 非常簡(jiǎn)單,不過(guò)設(shè)計(jì)到一些小竅門。 下面的 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
}

要使用這個(gè)函數(shù),你需要獲取傳遞過(guò)來(lái)的某個(gè)已存在 Python 調(diào)用的引用。 有很多種方法可以讓你這樣做, 比如將一個(gè)可調(diào)用對(duì)象傳給一個(gè)擴(kuò)展模塊或直接寫 C 代碼從已存在模塊中提取出來(lái)。

下面是一個(gè)簡(jiǎn)單例子用來(lái)掩飾從一個(gè)嵌入的 Python 解釋器中調(diào)用一個(gè)函數(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 可以教你怎樣做(不過(guò)在你機(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)行會(huì)產(chǎn)生類似下面的輸出:

0.00 0.00
0.10 0.01
0.20 0.04
0.30 0.09
0.40 0.16
...

下面是一個(gè)稍微不同的例子,展示了一個(gè)擴(kuò)展函數(shù), 它接受一個(gè)可調(diào)用對(duì)象和其他參數(shù),并將它們傳遞給 call_func() 來(lái)做測(cè)試:

/* 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);
}

使用這個(gè)擴(kuò)展函數(shù),你要像下面這樣測(cè)試它:

>>> import sample
>>> def add(x,y):
...     return x+y
...
>>> sample.call_func(add,3,4)
7.0
>>>

討論

如果你在 C 語(yǔ)言中調(diào)用 Python,要記住最重要的是 C 語(yǔ)言會(huì)是主體。 也就是說(shuō),C 語(yǔ)言負(fù)責(zé)構(gòu)造參數(shù)、調(diào)用 Python 函數(shù)、檢查異常、檢查類型、提取返回值等。

作為第一步,你必須先有一個(gè)表示你將要調(diào)用的 Python 可調(diào)用對(duì)象。 這可以是一個(gè)函數(shù)、類、方法、內(nèi)置方法或其他任意實(shí)現(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 代碼里處理錯(cuò)誤你需要格外的小心。一般來(lái)講,你不能僅僅拋出一個(gè) Python 異常。 錯(cuò)誤應(yīng)該使用 C 代碼方式來(lái)被處理。在這里,我們打算將對(duì)錯(cuò)誤的控制傳給一個(gè)叫 abort()的錯(cuò)誤處理器。 它會(huì)結(jié)束掉整個(gè)程序,在真實(shí)環(huán)境下面你應(yīng)該要處理的更加優(yōu)雅些(返回一個(gè)狀態(tài)碼)。 你要記住的是在這里 C 是主角,因此并沒(méi)有跟拋出異常相對(duì)應(yīng)的操作。 錯(cuò)誤處理是你在編程時(shí)必須要考慮的事情。

調(diào)用一個(gè)函數(shù)相對(duì)來(lái)講很簡(jiǎn)單——只需要使用 PyObject_Call() , 傳一個(gè)可調(diào)用對(duì)象給它、一個(gè)參數(shù)元組和一個(gè)可選的關(guān)鍵字字典。 要構(gòu)建參數(shù)元組或字典,你可以使用 Py_BuildValue(),如下:

double call_func(PyObject *func, double x, double y) {
  PyObject *args;
  PyObj