2.8. C とのインターフェース

著者: Valentin Haenel

この章は (主に C/C++ の) ネイティブコードを Python から利用できるようにするためのいくつかの異なるルートへの 導入 を含みます、このプロセスがよく ラッピング と呼ばれます。この章でのゴールはどんな技術が存在し、それぞれにどんな利点と欠点があるのかといった感触を得てもらい、必要なことに最適なものを選べるようにすることです。一度ラッピングを始めたらどの場合でも、選択した技術のドキュメントを参照することになると思います。

2.8.1. 導入

この章では以下の技術について扱います:

4つの技術はおそらく最も知られたものでしょう、その1つ Cython は最も先進的で最初に利用することを考えるものでしょう。ラッピングの問題を別の角度から見たい場合には、他のものも同じく重要でしょう。さらに他の選択肢もありますが、上記の内1つを理解していれば、要求に合わせて選択した技術を評価できるようになるでしょう。

以下の基準は技術を評価する際に有用でしょう:

  • 追加のライブラリが必要か?

  • コードは自動生成されるか?

  • コンパイルする必要があるか?

  • Numpy 配列と連携するための良いサポートが得られるか?

  • C++ をサポートしているか?

決心する前にはユースケースを考慮する必要があります。ネイティブコードに向かうときにはたいてい2つのユースケースが浮び上がります:

  • 既存の C/C++ コードを借用する必要がある、理由として既存のコーロを使いたい、あるいは高速化のために

  • Python コードが遅く、内部のループをネイティブコードに落し込みたい

各技術を math.hcos 関数をラップして、実演してみましょう。この例はささいな例ですが、ラッピングの策の基本を示すのにうまく動いてくれます。各技術は Numpy のサポートを何らかの形で持っているので、この例を使うことで余弦をある種の配列で計算できることも示してくれます。

最後に一言、短い注意を:

  • これらの技術全て Python インタプリタをクラッシュさせる(segmentation fault)可能性があります、これらは(たいていの場合) C コードのバグによって起きます。

  • 全ての例が Linux で行なわれますが、他の OS でも動く べき です。

  • ほとんどの例で C コンパイラが必要になります。

2.8.2. Python-C-Api

Python-C-API は Python の標準インタプリタ(Cython として知られる) の基礎となるものです。この API を利用することで C や C++ で Python の拡張モジュールを書くことができます。当然これらの拡張は、言語の互換性の利点を活かして、C や C++ の任意の関数を呼び出すことができます。

Python C API を利用する際には、たいてい常套句的なコードを多く書くことになります、関数に与えられた引数をパースし、その後に返り値の型を組み立てます。

利点

  • 追加ライブラリが不要

  • たくさんの低レベルな制御できる

  • 完全に C++ から利用できる

欠点

  • 相当の労力が必要になるでしょう

  • コードのオーバヘッドが多い

  • 必ず複雑になる

  • 高いメンテナンスコスト

  • Python バージョン間で C-API が変更された場合の前方互換性

  • 参照カウントのバグは簡単に作ることができ追い詰めるのは非常に困難。

注釈

Python の C-API 例を、主に教訓のために、ここに挙げます。多くの他の技術がこれに実際に依存しており、それらがどう動くのかを理解するには良いでしょう。99% のユースケースでは他のテクニックを使う方がいいでしょう。

注釈

参照カウントのバグは作るのは簡単で追い詰めるのは困難なので、本当に Python C-API が必要な人は Python の公式ドキュメントの section about objects, types and reference counts を読むべきです。さらに cpychecker という名前のツールがあり、参照カウントでよく遭遇するエラーを見つけるのを助けてくれます。

2.8.2.1. 例

以下の C 拡張モジュールは標準数学ライブラリから cos 関数を Python で利用可能にしてくれます:

/*  Example of wrapping cos function from math.h with the Python-C-API. */
#include <Python.h>
#include <math.h>
/* wrapped cosine function */
static PyObject* cos_func(PyObject* self, PyObject* args)
{
double value;
double answer;
/* parse the input, from python float to c double */
if (!PyArg_ParseTuple(args, "d", &value))
return NULL;
/* if the above function returns -1, an appropriate Python exception will
* have been set, and the function simply returns NULL
*/
/* call cos from libm */
answer = cos(value);
/* construct the output from cos, from c double to python float */
return Py_BuildValue("f", answer);
}
/* define functions in module */
static PyMethodDef CosMethods[] =
{
{"cos_func", cos_func, METH_VARARGS, "evaluate the cosine"},
{NULL, NULL, 0, NULL}
};
/* module initialization */
PyMODINIT_FUNC
initcos_module(void)
{
(void) Py_InitModule("cos_module", CosMethods);
}

見てわかるように、常套句的なコードがたくさんあり、引数と返り値の型を配置するためやモジュール初期化のために «もみほぐします»。いくつかは拡張が成長することで返済できますが、それぞれの(複数の)関数に対する常套句は以前として必要になります。

python の標準のビルドシステム distutils は C拡張の setup.py からのコンパイルをサポートしており、いくらか便利です:

from distutils.core import setup, Extension
# define the extension module
cos_module = Extension('cos_module', sources=['cos_module.c'])
# run the setup
setup(ext_modules=[cos_module])

こうやってコンパイルできます:

$ cd advanced/interfacing_with_c/python_c_api
$ ls
cos_module.c setup.py
$ python setup.py build_ext --inplace
running build_ext
building 'cos_module' extension
creating build
creating build/temp.linux-x86_64-2.7
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/include/python2.7 -c cos_module.c -o build/temp.linux-x86_64-2.7/cos_module.o
gcc -pthread -shared build/temp.linux-x86_64-2.7/cos_module.o -L/home/esc/anaconda/lib -lpython2.7 -o /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/python_c_api/cos_module.so
$ ls
build/ cos_module.c cos_module.so setup.py
  • build_ext で拡張モジュールをビルドできます

  • --inplace でコンパイルした拡張を現在のディレクトリに出力します

cos_module.so ファイルはコンパイルした拡張を含み、IPython インタプリタから読み込むことができます:

In [1]: import cos_module
In [2]: cos_module?
Type: module
String Form:<module 'cos_module' from 'cos_module.so'>
File: /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/python_c_api/cos_module.so
Docstring: <no docstring>
In [3]: dir(cos_module)
Out[3]: ['__doc__', '__file__', '__name__', '__package__', 'cos_func']
In [4]: cos_module.cos_func(1.0)
Out[4]: 0.5403023058681398
In [5]: cos_module.cos_func(0.0)
Out[5]: 1.0
In [6]: cos_module.cos_func(3.14159265359)
Out[7]: -1.0

それでは、どのくらい頑丈か確認してみましょう:

In [10]: cos_module.cos_func('foo')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-10-11bee483665d> in <module>()
----> 1 cos_module.cos_func('foo')
TypeError: a float is required

2.8.2.2. Numpy のサポート

Python の C-API と同様に Numpy 自身も Numpy-C-API という C拡張を実装しています。この API を利用して、カスタムした C 拡張を書くことで Numpy の配列を C から作成し、操作することができます。 :ref:`advanced_numpy`_ について参照して下さい。

注釈

Numpy C-API を使う必要があれば ArraysIterators についてのドキュメントを参照してください。

以下の例は Numpy arraysを関数の引数を渡す方法や (古い) Numpy-C-API を使って Numpy 配列を反復する方法を示しています。この例では配列を引数にとり math.h の余弦関数を適用し、結果を新しい配列として返します。

/*  Example of wrapping the cos function from math.h using the Numpy-C-API. */
#include <Python.h>
#include <numpy/arrayobject.h>
#include <math.h>
/* wrapped cosine function */
static PyObject* cos_func_np(PyObject* self, PyObject* args)
{
PyArrayObject *in_array;
PyObject *out_array;
NpyIter *in_iter;
NpyIter *out_iter;
NpyIter_IterNextFunc *in_iternext;
NpyIter_IterNextFunc *out_iternext;
/* parse single numpy array argument */
if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &in_array))
return NULL;
/* construct the output array, like the input array */
out_array = PyArray_NewLikeArray(in_array, NPY_ANYORDER, NULL, 0);
if (out_array == NULL)
return NULL;
/* create the iterators */
in_iter = NpyIter_New(in_array, NPY_ITER_READONLY, NPY_KEEPORDER,
NPY_NO_CASTING, NULL);
if (in_iter == NULL)
goto fail;
out_iter = NpyIter_New((PyArrayObject *)out_array, NPY_ITER_READWRITE,
NPY_KEEPORDER, NPY_NO_CASTING, NULL);
if (out_iter == NULL) {
NpyIter_Deallocate(in_iter);
goto fail;
}
in_iternext = NpyIter_GetIterNext(in_iter, NULL);
out_iternext = NpyIter_GetIterNext(out_iter, NULL);
if (in_iternext == NULL || out_iternext == NULL) {
NpyIter_Deallocate(in_iter);
NpyIter_Deallocate(out_iter);
goto fail;
}
double ** in_dataptr = (double **) NpyIter_GetDataPtrArray(in_iter);
double ** out_dataptr = (double **) NpyIter_GetDataPtrArray(out_iter);
/* iterate over the arrays */
do {
**out_dataptr = cos(**in_dataptr);
} while(in_iternext(in_iter) && out_iternext(out_iter));
/* clean up and return the result */
NpyIter_Deallocate(in_iter);
NpyIter_Deallocate(out_iter);
Py_INCREF(out_array);
return out_array;
/* in case bad things happen */
fail:
Py_XDECREF(out_array);
return NULL;
}
/* define functions in module */
static PyMethodDef CosMethods[] =
{
{"cos_func_np", cos_func_np, METH_VARARGS,
"evaluate the cosine on a numpy array"},
{NULL, NULL, 0, NULL}
};
/* module initialization */
PyMODINIT_FUNC
initcos_module_np(void)
{
(void) Py_InitModule("cos_module_np", CosMethods);
/* IMPORTANT: this must be called */
import_array();
}

コンパイルするために再度 distutils を利用することができます。ただし Numpy のヘッダが include されているか :func:numpy.get_include を利用して確認する必要があります。

from distutils.core import setup, Extension
import numpy
# define the extension module
cos_module_np = Extension('cos_module_np', sources=['cos_module_np.c'],
include_dirs=[numpy.get_include()])
# run the setup
setup(ext_modules=[cos_module_np])

実際に動作するか確認するために以下のテストスクリプトを走らせます:

import cos_module_np
import numpy as np
import pylab
x = np.arange(0, 2 * np.pi, 0.1)
y = cos_module_np.cos_func_np(x)
pylab.plot(x, y)
pylab.show()

以下の図のような結果になる必要があります:

../../_images/test_cos_module_np.png

2.8.3. Ctypes

Ctypes は Python の 外部関数ライブラリ です。これにより C 互換のデータ型や DLL や共有ライブラリ内の関数を呼び出すことができます。これらの関数を pure Python 内でラップするのにも利用できます。

利点

  • Python 標準ライブラリの一部

  • コンパイルする必要がない

  • 完全に Python の中でコードをラップする

欠点

  • ラップするのに必要なコードが共有ライブラリとして (おおざっぱにいえば Windows では *.dll に、Linux では *.so、Mac OS X では *.dylib) 利用可能となっている必要がある

  • C++ のサポートは十分ではない

2.8.3.1. 例

宣伝通り、ラッパーコードは pure Python の中に書けます。

""" Example of wrapping cos function from math.h using ctypes. """
import ctypes
from ctypes.util import find_library
# find and load the library
libm = ctypes.cdll.LoadLibrary(find_library('m'))
# set the argument type
libm.cos.argtypes = [ctypes.c_double]
# set the return type
libm.cos.restype = ctypes.c_double
def cos_func(arg):
''' Wrapper for cos from math.h '''
return libm.cos(arg)
  • ライブラリの探索と読み込みは OS によって違います、詳細については the documentation を確認して下さい

  • 数学ライブラリはコンパイルされた形式で既にシステムに存在しているので少し紛らわしいところがあります。自前のライブラリをラップする場合は、それをまずコンパイルすることになります、それには追加の労力が必要になるかもしれませんし、ならないかもしれません。

ひとまずこれを使ってみましょう、前と同じように:

In [1]: import cos_module
In [2]: cos_module?
Type: module
String Form:<module 'cos_module' from 'cos_module.py'>
File: /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/ctypes/cos_module.py
Docstring: <no docstring>
In [3]: dir(cos_module)
Out[3]:
['__builtins__',
'__doc__',
'__file__',
'__name__',
'__package__',
'cos_func',
'ctypes',
'find_library',
'libm']
In [4]: cos_module.cos_func(1.0)
Out[4]: 0.5403023058681398
In [5]: cos_module.cos_func(0.0)
Out[5]: 1.0
In [6]: cos_module.cos_func(3.14159265359)
Out[6]: -1.0

前の例にあるようにこのコードはいくらか頑強ですが、型が何なのか教えてくれないのでエラーメッセージはわかりやすくありません。

In [7]: cos_module.cos_func('foo')
---------------------------------------------------------------------------
ArgumentError Traceback (most recent call last)
<ipython-input-7-11bee483665d> in <module>()
----> 1 cos_module.cos_func('foo')
/home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/ctypes/cos_module.py in cos_func(arg)
12 def cos_func(arg):
13 ''' Wrapper for cos from math.h '''
---> 14 return libm.cos(arg)
ArgumentError: argument 1: <type 'exceptions.TypeError'>: wrong type

2.8.3.2. Numpy のサポート

Numpy は ctypes と連携するためのいくつかのサポートを含んでいます。特に Numpy 配列の特定の属性を ctypes のデータ型としてエクスポートするサポートや C 配列を Numpy 配列に変換する、あるいはその逆をする関数のようなサポートがあります。

より詳しくは Numpy Cookbook の節の numpy.ndarray.ctypes の API ドキュメント、 numpy.ctypeslib を参照してください。

以下の例では、ライブラリ内の関数で、入出力の配列をとり、入力配列の余弦を計算し、結果を出力配列に格納するものを考えます。

ライブラリは以下のヘッダファイルで構成されます (厳密にはこの例では不要ですが、万全を期すために載せています):

void cos_doubles(double * in_array, double * out_array, int size);

関数は次の C ソースファイルで実装されています:

#include <math.h>
/* Compute the cosine of each element in in_array, storing the result in
* out_array. */
void cos_doubles(double * in_array, double * out_array, int size){
int i;
for(i=0;i<size;i++){
out_array[i] = cos(in_array[i]);
}
}

ライブラリは pure C なので distils でコンパイルすることはできません、 makegcc の組み合わせてやる必要があります:

m.PHONY : clean
libcos_doubles.so : cos_doubles.o
gcc -shared -Wl,-soname,libcos_doubles.so -o libcos_doubles.so cos_doubles.o
cos_doubles.o : cos_doubles.c
gcc -c -fPIC cos_doubles.c -o cos_doubles.o
clean :
-rm -vf libcos_doubles.so cos_doubles.o cos_doubles.pyc

(Linux 上で) 共有ライブラリ libcos_doubles.so にコンパイルすます:

$ ls
cos_doubles.c cos_doubles.h cos_doubles.py makefile test_cos_doubles.py
$ make
gcc -c -fPIC cos_doubles.c -o cos_doubles.o
gcc -shared -Wl,-soname,libcos_doubles.so -o libcos_doubles.so cos_doubles.o
$ ls
cos_doubles.c cos_doubles.o libcos_doubles.so* test_cos_doubles.py
cos_doubles.h cos_doubles.py makefile

ここで、このライブラリを ctypes の (ある種の) Numpy 配列の直接的なサポートを使ってラップすることにします。

""" Example of wrapping a C library function that accepts a C double array as
input using the numpy.ctypeslib. """
import numpy as np
import numpy.ctypeslib as npct
from ctypes import c_int
# input type for the cos_doubles function
# must be a double array, with single dimension that is contiguous
array_1d_double = npct.ndpointer(dtype=np.double, ndim=1, flags='CONTIGUOUS')
# load the library, using numpy mechanisms
libcd = npct.load_library("libcos_doubles", ".")
# setup the return types and argument types
libcd.cos_doubles.restype = None
libcd.cos_doubles.argtypes = [array_1d_double, array_1d_double, c_int]
def cos_doubles_func(in_array, out_array):
return libcd.cos_doubles(in_array, out_array, len(in_array))
  • 連続した単一の次元の Numpy 配列特有の制限のために C 関数にはこのようなバッファが必要になることに注意してください。

  • また、出力配列が事前に割り当てられている必要があることにも注意してください、例えば numpy.zeros() とこの関数はバッファに書き込むことになります。

  • cos_doubles 関数の元々のシグネチャは ARRAY, ARRAY, int ですが、最終的な cos_doubles_func は2つの Numpy 配列のみを引数としてとります。

そして、前と同様に、動作確認します:

import numpy as np
import pylab
import cos_doubles
x = np.arange(0, 2 * np.pi, 0.1)
y = np.empty_like(x)
cos_doubles.cos_doubles_func(x, y)
pylab.plot(x, y)
pylab.show()
../../_images/test_cos_doubles.png

2.8.4. SWIG

SWIG, は Simplified Wrapper Interface Generator, で C や C++ で書かれたプログラムを、Python を含む様々な高レベル言語と組み合わせるためのソフトウェア開発ツールです。SWIG で重要なことはラッパーコードを自動生成できるところです。これは開発という側面では利点となる一方、負担になることもあります。生成されたファイルは大きくかつ人間にとって読みにくいものになりがちで、ラッピングプロセスの結果として複数のレベルの不明瞭さを持ち、理解するのにいくらか工夫を重ねることになります。

注釈

自動生成された C コードは Python C-API を利用します。

利点

  • ヘッダを与えることで自動的にライブラリ全体をラップできます

  • C++ とあわせてうまく動きます

欠点

  • たくさんのファイルが自動生成されます

  • おかしくなったときのデバッグが大変です

  • 学習曲線が急

2.8.4.1. 例

cos 関数が cos_module で使うことができ、モジュールはソースファイル cos_module.cc で書かれているとします:

#include <math.h>
double cos_func(double arg){
return cos(arg);
}

そして cos_module.h のヘッダファイル:

double cos_func(double arg);

ゴールは cos_func を Python に見えるようにすることです。これを実現するために SWIG を使います、SWIG への指示を含む インターフェースファイル を書く必要があります。

/*  Example of wrapping cos function from math.h using SWIG. */
%module cos_module
%{
/* the resulting C file should be built as a python extension */
#define SWIG_FILE_WITH_INIT
/* Includes the header in the wrapper code */
#include "cos_module.h"
%}
/* Parse the header file to generate wrappers */
%include "cos_module.h"

見てわかるように、ここまででコードをたくさん書く必要はありませんでした。この例ではヘッダファイルをインターフェースファイルの中で単にインクルードすることで、関数を Python から見えるようにできます。しかし、SWIG はより細かい粒度でヘッダファイル内の関数を含めたり除いたりすることができます、詳細はドキュメントを確認してください。

コンパイルされたラッパーは2段階のプロセスで生成されます:

  1. swig のインターフェースファイルに対して走らせて cos_module_wrap.ccos_module.py を生成します、前者は自動生成された Python C 拡張のソースファイルで後者は自動生成された pure Python モジュールです。

  2. cos_module_wrap.c_cos_module.so にコンパイルします。幸運にも distutils は SWIG のインターフェースファイルをどう扱うかを知っているので setup.py は単純に:

from distutils.core import setup, Extension
setup(ext_modules=[Extension("_cos_module",
sources=["cos_module.c", "cos_module.i"])])
$ cd advanced/interfacing_with_c/swig
$ ls
cos_module.c cos_module.h cos_module.i setup.py
$ python setup.py build_ext --inplace
running build_ext
building '_cos_module' extension
swigging cos_module.i to cos_module_wrap.c
swig -python -o cos_module_wrap.c cos_module.i
creating build
creating build/temp.linux-x86_64-2.7
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/include/python2.7 -c cos_module.c -o build/temp.linux-x86_64-2.7/cos_module.o
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/include/python2.7 -c cos_module_wrap.c -o build/temp.linux-x86_64-2.7/cos_module_wrap.o
gcc -pthread -shared build/temp.linux-x86_64-2.7/cos_module.o build/temp.linux-x86_64-2.7/cos_module_wrap.o -L/home/esc/anaconda/lib -lpython2.7 -o /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/swig/_cos_module.so
$ ls
build/ cos_module.c cos_module.h cos_module.i cos_module.py _cos_module.so* cos_module_wrap.c setup.py

これで cos_module を前の例でやったように読み込んで実行することができます:

In [1]: import cos_module
In [2]: cos_module?
Type: module
String Form:<module 'cos_module' from 'cos_module.py'>
File: /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/swig/cos_module.py
Docstring: <no docstring>
In [3]: dir(cos_module)
Out[3]:
['__builtins__',
'__doc__',
'__file__',
'__name__',
'__package__',
'_cos_module',
'_newclass',
'_object',
'_swig_getattr',
'_swig_property',
'_swig_repr',
'_swig_setattr',
'_swig_setattr_nondynamic',
'cos_func']
In [4]: cos_module.cos_func(1.0)
Out[4]: 0.5403023058681398
In [5]: cos_module.cos_func(0.0)
Out[5]: 1.0
In [6]: cos_module.cos_func(3.14159265359)
Out[6]: -1.0

頑強さを再び確認してみましょう、そしてわかりやすいエラーメッセージを見ることになります(厳密にいえば Python には double 型はないのですが):

In [7]: cos_module.cos_func('foo')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-7-11bee483665d> in <module>()
----> 1 cos_module.cos_func('foo')
TypeError: in method 'cos_func', argument 1 of type 'double'

2.8.4.2. Numpy のサポート

Numpy は numpy.i ファイルで support for SWIG を提供しています。このインターフェースファイルは様々な 型マップ を提供しており、Numpy 配列と C 配列の変換をサポートしています。以下の例では型マップが実際にどう動作するか手っ取り早くみてみます。

ctypes の例と同じく cos_doubles 関数を使います:

void cos_doubles(double * in_array, double * out_array, int size);
#include <math.h>
/* Compute the cosine of each element in in_array, storing the result in
* out_array. */
void cos_doubles(double * in_array, double * out_array, int size){
int i;
for(i=0;i<size;i++){
out_array[i] = cos(in_array[i]);
}
}

これは以下の SWIG インターフェースファイルを利用して cos_doubles_func をラップします:

/*  Example of wrapping a C function that takes a C double array as input using
* numpy typemaps for SWIG. */
%module cos_doubles
%{
/* the resulting C file should be built as a python extension */
#define SWIG_FILE_WITH_INIT
/* Includes the header in the wrapper code */
#include "cos_doubles.h"
%}
/* include the numpy typemaps */
%include "numpy.i"
/* need this for correct module initialization */
%init %{
import_array();
%}
/* typemaps for the two arrays, the second will be modified in-place */
%apply (double* IN_ARRAY1, int DIM1) {(double * in_array, int size_in)}
%apply (double* INPLACE_ARRAY1, int DIM1) {(double * out_array, int size_out)}
/* Wrapper for cos_doubles that massages the types */
%inline %{
/* takes as input two numpy arrays */
void cos_doubles_func(double * in_array, int size_in, double * out_array, int size_out) {
/* calls the original funcion, providing only the size of the first */
cos_doubles(in_array, out_array, size_in);
}
%}
  • Numpy の型マップを利用するために numpy.i ファイルをインクルードする必要があります。

  • import_array() に気づきましたか、これは Numpy C API の例で既に遭遇しています。

  • 型マップは ARRAY, SIZE のシグネチャのみをサポートしているので、 cos_doubles を 2つの配列とサイズを入力にとる cos_doubles_func としてラップします。

  • 単純な SWIG の例と反対に cos_doubles.h ヘッダをインクルードしません。Python からは cos_doubles_func 経由で見せればいいので、Python から見えるようにする必要はないわけです。

そして前と同様に disutils を使ってラップします:

from distutils.core import setup, Extension
import numpy
setup(ext_modules=[Extension("_cos_doubles",
sources=["cos_doubles.c", "cos_doubles.i"],
include_dirs=[numpy.get_include()])])

前と同様に include_dirs を使って場所を指定する必要があります。

$ ls
cos_doubles.c cos_doubles.h cos_doubles.i numpy.i setup.py test_cos_doubles.py
$ python setup.py build_ext -i
running build_ext
building '_cos_doubles' extension
swigging cos_doubles.i to cos_doubles_wrap.c
swig -python -o cos_doubles_wrap.c cos_doubles.i
cos_doubles.i:24: Warning(490): Fragment 'NumPy_Backward_Compatibility' not found.
cos_doubles.i:24: Warning(490): Fragment 'NumPy_Backward_Compatibility' not found.
cos_doubles.i:24: Warning(490): Fragment 'NumPy_Backward_Compatibility' not found.
creating build
creating build/temp.linux-x86_64-2.7
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include -I/home/esc/anaconda/include/python2.7 -c cos_doubles.c -o build/temp.linux-x86_64-2.7/cos_doubles.o
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include -I/home/esc/anaconda/include/python2.7 -c cos_doubles_wrap.c -o build/temp.linux-x86_64-2.7/cos_doubles_wrap.o
In file included from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1722,
from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:17,
from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/arrayobject.h:15,
from cos_doubles_wrap.c:2706:
/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/npy_deprecated_api.h:11:2: warning: #warning "Using deprecated NumPy API, disable it by #defining NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION"
gcc -pthread -shared build/temp.linux-x86_64-2.7/cos_doubles.o build/temp.linux-x86_64-2.7/cos_doubles_wrap.o -L/home/esc/anaconda/lib -lpython2.7 -o /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/swig_numpy/_cos_doubles.so
$ ls
build/ cos_doubles.h cos_doubles.py cos_doubles_wrap.c setup.py
cos_doubles.c cos_doubles.i _cos_doubles.so* numpy.i test_cos_doubles.py

そして、前と同様に、動作確認します:

import numpy as np
import pylab
import cos_doubles
x = np.arange(0, 2 * np.pi, 0.1)
y = np.empty_like(x)
cos_doubles.cos_doubles_func(x, y)
pylab.plot(x, y)
pylab.show()
../../_images/test_cos_doubles1.png

2.8.5. Cython

Cython は C 拡張を書くための Python 風言語であり、この言語の先進的なコンパイラでもあります。Cython 言語 は Python のスーパーセットであり、C 関数の呼び出しや変数やクラスの属性を c の型でアノテーションするいった追加要素を含んでいます。そういう意味では 型を持った Python と呼ぶ人もいるでしょう。

ネイティブコードをラップする基本的なユースケースに加えて、Cython は対話的な最適化とよばれる、追加のユースケースをサポートしています。要するに pure Python スクリプトから始めて逐次的に Cython の型をボトルネックコードに追加して、実際に問題になっていいるコードパスを最適化します。

ある意味では、コードを自動生成するという点ではSWIG と似ています、またラッピングコードが(ほとんど) Python で書けるという点では ctypes に似ています。

他のコードを自動生成する方法(例えば SWIG)はデバッグが困難ですが Cython は GNU デバッガーの拡張があり、Python と Cython そして C コードをデバッグするのを助けてくれます。

注釈

自動生成された C コードは Python C-API を利用します。

利点

  • C 拡張を書くための Python 風言語

  • 自動生成されるコード

  • 逐次的な最適化のサポート

  • GNU デバッガ拡張を含む

  • C++ のサポート (バージョン 0.13 から)

欠点

  • 必ず複雑になる

  • 追加のライブラリが必要(ただしビルドするときのみで、生成された C ファイルとともに出荷することで克服できる)

2.8.5.1. 例

cos_module のための Cython コードは cos_moduke.pyx に含まれます。

""" Example of wrapping cos function from math.h using Cython. """
cdef extern from "math.h":
double cos(double arg)
def cos_func(arg):
return cos(arg)

ここで追加のキーワード cdefextern に注意してください。同様に cos_func は pure Python です。

再び標準の distutils を利用できますが、ここでいくつか Cython.Distutils から追加する必要があります:

from distutils.core import setup, Extension
from Cython.Distutils import build_ext
setup(
cmdclass={'build_ext': build_ext},
ext_modules=[Extension("cos_module", ["cos_module.pyx"])]
)

コンパイル:

$ cd advanced/interfacing_with_c/cython
$ ls
cos_module.pyx setup.py
$ python setup.py build_ext --inplace
running build_ext
cythoning cos_module.pyx to cos_module.c
building 'cos_module' extension
creating build
creating build/temp.linux-x86_64-2.7
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/include/python2.7 -c cos_module.c -o build/temp.linux-x86_64-2.7/cos_module.o
gcc -pthread -shared build/temp.linux-x86_64-2.7/cos_module.o -L/home/esc/anaconda/lib -lpython2.7 -o /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/cython/cos_module.so
$ ls
build/ cos_module.c cos_module.pyx cos_module.so* setup.py

実行:

In [1]: import cos_module
In [2]: cos_module?
Type: module
String Form:<module 'cos_module' from 'cos_module.so'>
File: /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/cython/cos_module.so
Docstring: <no docstring>
In [3]: dir(cos_module)
Out[3]:
['__builtins__',
'__doc__',
'__file__',
'__name__',
'__package__',
'__test__',
'cos_func']
In [4]: cos_module.cos_func(1.0)
Out[4]: 0.5403023058681398
In [5]: cos_module.cos_func(0.0)
Out[5]: 1.0
In [6]: cos_module.cos_func(3.14159265359)
Out[6]: -1.0

そして頑強さのテストを少々、わかりやすいエラーメッセージを見ることができます:

In [7]: cos_module.cos_func('foo')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-7-11bee483665d> in <module>()
----> 1 cos_module.cos_func('foo')
/home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/cython/cos_module.so in cos_module.cos_func (cos_module.c:506)()
TypeError: a float is required

さらに Cython を完璧な C での数学関数宣言を備えて出荷することで、上記コードをより単純にすることのは注目に値します:

""" Simpler example of wrapping cos function from math.h using Cython. """
from libc.math cimport cos
def cos_func(arg):
return cos(arg)

この場合 cimport 文は cos 関数をインポートするのに利用されています。

2.8.5.2. Numpy のサポート

Cython は numpy.pyx ファイルで Numpy をサポートし、これによって Numpy 配列の型を Cython コードに追加することができます、つまり変数 iint 型として指定するように変数 adtype を与えた numpy.ndarray 型として指定することができます。同じく境界チェックのようなある種の最適化もサポートされます。 Cython documentation の対応する節を参照して下さい。Numpy の配列を C 配列として Cython でラップした C 関数に渡したい場合は Cython wiki に記載があります。

以下の例では馴染み深い cos_doubles を Cython を使ってラップします。

void cos_doubles(double * in_array, double * out_array, int size);
#include <math.h>
/* Compute the cosine of each element in in_array, storing the result in
* out_array. */
void cos_doubles(double * in_array, double * out_array, int size){
int i;
for(i=0;i<size;i++){
out_array[i] = cos(in_array[i]);
}
}

以下の Cython コードを利用して cos_doubles_func としてラップされます:

""" Example of wrapping a C function that takes C double arrays as input using
the Numpy declarations from Cython """
# cimport the Cython declarations for numpy
cimport numpy as np
# if you want to use the Numpy-C-API from Cython
# (not strictly necessary for this example, but good practice)
np.import_array()
# cdefine the signature of our c function
cdef extern from "cos_doubles.h":
void cos_doubles (double * in_array, double * out_array, int size)
# create the wrapper code, with numpy type annotations
def cos_doubles_func(np.ndarray[double, ndim=1, mode="c"] in_array not None,
np.ndarray[double, ndim=1, mode="c"] out_array not None):
cos_doubles(<double*> np.PyArray_DATA(in_array),
<double*> np.PyArray_DATA(out_array),
in_array.shape[0])

distutils を利用してコンパイルできます:

from distutils.core import setup, Extension
import numpy
from Cython.Distutils import build_ext
setup(
cmdclass={'build_ext': build_ext},
ext_modules=[Extension("cos_doubles",
sources=["_cos_doubles.pyx", "cos_doubles.c"],
include_dirs=[numpy.get_include()])],
)
  • 前の Numpy の例でコンパイルしたのと同様に include_dirs オプションが必要です。

$ ls
cos_doubles.c cos_doubles.h _cos_doubles.pyx setup.py test_cos_doubles.py
$ python setup.py build_ext -i
running build_ext
cythoning _cos_doubles.pyx to _cos_doubles.c
building 'cos_doubles' extension
creating build
creating build/temp.linux-x86_64-2.7
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include -I/home/esc/anaconda/include/python2.7 -c _cos_doubles.c -o build/temp.linux-x86_64-2.7/_cos_doubles.o
In file included from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1722,
from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:17,
from /home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/arrayobject.h:15,
from _cos_doubles.c:253:
/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/npy_deprecated_api.h:11:2: warning: #warning "Using deprecated NumPy API, disable it by #defining NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION"
/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include/numpy/__ufunc_api.h:236: warning: ‘_import_umath’ defined but not used
gcc -pthread -fno-strict-aliasing -g -O2 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/esc/anaconda/lib/python2.7/site-packages/numpy/core/include -I/home/esc/anaconda/include/python2.7 -c cos_doubles.c -o build/temp.linux-x86_64-2.7/cos_doubles.o
gcc -pthread -shared build/temp.linux-x86_64-2.7/_cos_doubles.o build/temp.linux-x86_64-2.7/cos_doubles.o -L/home/esc/anaconda/lib -lpython2.7 -o /home/esc/git-working/scipy-lecture-notes/advanced/interfacing_with_c/cython_numpy/cos_doubles.so
$ ls
build/ _cos_doubles.c cos_doubles.c cos_doubles.h _cos_doubles.pyx cos_doubles.so* setup.py test_cos_doubles.py

そして、前と同様に、動作確認します:

import numpy as np
import pylab
import cos_doubles
x = np.arange(0, 2 * np.pi, 0.1)
y = np.empty_like(x)
cos_doubles.cos_doubles_func(x, y)
pylab.plot(x, y)
pylab.show()
../../_images/test_cos_doubles2.png

2.8.6. まとめ

この節では4つの異なるネイティブコードと連携するテクニックについて扱いました。テクニックをいくつかの側面でおおざっぱに以下の表にまとめています。

x

CPython の一部

コンパイルされる

自動生成される

Numpy のサポート

Python-C-API True True False True
Ctypes True False False True
Swig False True True True
Cython False True True True

全てのテクニックの中で、Cython が最も現代的で進んでいます。特に Python コードに型を加えることで逐次的に最適化できるのは傑出しています。

2.8.7. 次に読むもの、参考文献

  • Gaël Varoquaux’s blog post about avoiding data copies ではメモリ管理を賢く扱うための見識が提供されています。大きなデータセットの問題に陥ったことがあるなら、インスピレーションとともに問題にたちかえる道標になります。

2.8.8. 練習問題

この節は真新しい節なので、この練習問題は次に何をすべきか指し示すためのものです、そのため面白そうなものについて扱います。もし練習問題にいいアイデアがあれば、知らせてください!

  1. 各例のコードをダウンロードしてコンパイルし、各自のマシンで実行してみましょう。

  2. それぞえの例にささいな変更を加えて動作するか確認してみましょう (例 cossin に変える)

  3. ほとんどの例で、特に Numpy を含んだものは、壊れやすく入力のエラーに対する反応も悪いでしょう。例をクラッシュさせてみましょう、何が問題になるか考えて、解決策になりうるものを考えてみましょう。いくつかのアイデアがあります:

    1. 数値オーバーフロー

    2. 入出力の配列が異なる長さを持つ。

    3. 多次元配列。

    4. 空の配列

    5. double でない型の配列

  4. IPython のマジックコマンド %timeit を使って様々な方法の実行速度を測定しましょう

2.8.8.1. Python-C-API

  1. Numpy の例を、関数が2つの入力引数をとり、2つめは事前に割り当てた出力配列とします、他の Numpy の例でも同様にしましょう。

  2. 例を変更して、関数が1つの入力配列をとり、インプレースにそれを変更するようにしましょう。

  3. 新しい Numpy iterator protocol を使って例を直してみましょう。動く解答が得られたら github で pull-request を送ってください。

  4. 気付いているかもしれませんが Numpy-C-API の例は cos_doubles をラップしない Numpy の例だけで、代わりに cos 関数を直接 Numpy 配列に適用しています。この方法は他の方法と比べて利点がありますか?

  5. cos_doubles を Numpy-C-API でラップすることはできますか? 配列が正しい型を持つか確認する必要があるかもしれません、1次元なのか、メモリ上で連続か。

2.8.8.2. Ctypes

  1. Numpy の例を変更して cos_doubles_func で事前割り当てを扱い Numpy-C-API の例のようにしましょう。

2.8.8.3. SWIG

  1. SWIG が自動生成したコードをみてみましょう、どのくらい理解できましたか?

  2. Numpy の例を変更して cos_doubles_func で事前割り当てを扱い Numpy-C-API の例のようにしましょう。

  3. cos_doubles C 関数を変更して、割り当てた配列を返すようにしましょう。これを SWIG の型マップを利用してラップできますか? できないとすると何故ですか? この状況に対する対処法がありますか? (ヒント: 出力配列のサイズを知っているので、Numpy 配列を返ってくる double * から構築することはできそうです。)

2.8.8.4. Cython

  1. Cython の自動生成コードをみてみましょう。Cython がいれるコメントをいくつか詳しくみてみましょう。何が書いてありますか?

  2. Cython のドキュメントの Working with Numpy を読んで、Numpy を使う pure Python スクリプトを逐次的に最適化する方法について学びましょう。

  3. Numpy の例を変更して cos_doubles_func で事前割り当てを扱い Numpy-C-API の例のようにしましょう。