254


148

PythonでCライブラリをラップする:C、Cython、またはctypes?

PythonアプリケーションからCライブラリを呼び出したい。 API全体をラップするのではなく、私のケースに関連する関数とデータ型のみをラップします。 ご覧のとおり、3つの選択肢があります。

  1. Cで実際の拡張モジュールを作成します。 おそらく過剰であり、私は また、拡張機能の記述を学習するオーバーヘッドを回避したいです。

  2. Cythonを使用して、関連する部分を公開します CライブラリをPythonに。

  3. Pythonですべてを実行します。 http://docs.python.org/library/ctypes.html [ctypes]を使用して、外部ライブラリと通信します。

2)または3)のどちらが良い選択かわかりません。 3)の利点は、 `ctypes`が標準ライブラリの一部であり、結果のコードが純粋なPythonになることです。実際にその利点がどれほど大きいかはわかりません。

どちらの選択にも利点/欠点はありますか? どのアプローチをお勧めしますか?

'' '' '

*編集:*あなたのすべての答えに感謝します。彼らは同様のことをしたい人に良いリソースを提供します。 もちろん、単一のケースについてはまだ決定が行われています。「これは正しいことです」というような答えはありません。 私の場合は、おそらくctypesを使用しますが、他のプロジェクトでCythonを試してみるのも楽しみです。

真の答えが1つも存在しないため、答えを受け入れるのはいくぶんarbitrary意的です。 FogleBirdの答えを選んだのは、ctypesについての優れた洞察を提供するものであり、現在、最も投票数の多い答えでもあるからです。 ただし、すべての回答を読んで概要を把握することをお勧めします。

再度、感謝します。

11 回答


137


警告:Cythonコア開発者の意見は先にあります。

ほとんどの場合、ctypesよりもCythonをお勧めします。 その理由は、アップグレードパスがよりスムーズになったためです。 ctypesを使用する場合、最初は多くのことが簡単になります。コンパイル、ビルドの依存関係などを行わずに、プレーンなPythonでFFIコードを記述するのは確かにクールです。 しかし、ある時点で、ループまたはより長い一連の相互依存呼び出しのいずれかで、Cライブラリを頻繁に呼び出す必要があることがほぼ確実にわかるでしょう。そして、それをスピードアップしたいと思います。 それが、ctypesでそれができないことに気付くポイントです。 または、コールバック関数が必要で、Pythonコールバックコードがボトルネックになることがわかった場合は、それを高速化するか、Cに下げることもできます。 繰り返しになりますが、ctypesではできません。 そのため、その時点で言語を切り替えてコードの一部を書き直し、潜在的にPython / ctypesコードをプレーンCにリバースエンジニアリングする必要があります。そのため、そもそもプレーンPythonでコードを書くメリット全体を台無しにします。

Cython、OTOHを使用すると、ラッピングおよび呼び出しコードを自由に細くしたり太くしたりできます。 通常のPythonコードからCコードへの単純な呼び出しから開始できます。Cythonは、呼び出しオーバーヘッドを追加することなく、Pythonパラメーターの変換オーバーヘッドを非常に低くして、ネイティブC呼び出しに変換します。 Cライブラリへの高価な呼び出しが多すぎる時点でさらにパフォーマンスが必要なことに気付いたら、周囲のPythonコードに静的型の注釈を付け始め、CythonがCに直接最適化するようにできます。 または、CythonでCコードの一部の書き換えを開始して、呼び出しを回避し、ループをアルゴリズム的に特化して強化することができます。 また、高速のコールバックが必要な場合は、適切な署名を使用して関数を作成し、Cコールバックレジストリに直接渡します。 繰り返しますが、オーバーヘッドはありません。また、C呼び出しのパフォーマンスが向上します。 また、Cythonでコードを実際に十分に高速で取得できない場合は、C(またはC ++またはFortran)でコードの本当に重要な部分を書き換えて、Cythonコードから自然かつネイティブに呼び出すことを検討できます。 しかし、その後、これは本当に唯一の選択肢ではなく最後の手段になります。

そのため、ctypesは簡単なことを実行して、すぐに何かを実行するのに適しています。 ただし、物事が成長し始めるとすぐに、最初からCythonを使用した方が良いことに気付くでしょう。


104


「ctypes」は、すぐに実行するための最善の策であり、まだPythonを書いているので、一緒に作業するのは楽しいことです!

最近、ctypesを使用してUSBチップと通信するためのhttp://www.ftdichip.com/[FTDI]ドライバーをラップしましたが、それは素晴らしかったです。 私はそれをすべて完了し、1営業日未満で作業しました。 (必要な機能のみを実装しました。約15個の機能です)。

以前は、同じ目的でサードパーティのモジュールhttp://bleyer.org/pyusb/[PyUSB]を使用していました。 PyUSBは、実際のC / Python拡張モジュールです。 しかし、読み取り/書き込みのブロックを行っているときにPyUSBがGILをリリースしなかったため、問題が発生していました。 そのため、ネイティブ関数を呼び出すときにGILを解放するctypesを使用して独自のモジュールを作成しました。

注意すべきことの1つは、ctypesが使用しているライブラリ内の `#define`定数と内容ではなく、関数のみを認識するため、独自のコードでこれらの定数を再定義する必要があることです。

コードがどのように見えたかの例を以下に示します(多くを抜き取って、その要点を示すだけです):

from ctypes import *

d2xx = WinDLL('ftd2xx')

OK = 0
INVALID_HANDLE = 1
DEVICE_NOT_FOUND = 2
DEVICE_NOT_OPENED = 3

...

def openEx(serial):
    serial = create_string_buffer(serial)
    handle = c_int()
    if d2xx.FT_OpenEx(serial, OPEN_BY_SERIAL_NUMBER, byref(handle)) == OK:
        return Handle(handle.value)
    raise D2XXException

class Handle(object):
    def __init__(self, handle):
        self.handle = handle
    ...
    def read(self, bytes):
        buffer = create_string_buffer(bytes)
        count = c_int()
        if d2xx.FT_Read(self.handle, buffer, bytes, byref(count)) == OK:
            return buffer.raw[:count.value]
        raise D2XXException
    def write(self, data):
        buffer = create_string_buffer(data)
        count = c_int()
        bytes = len(data)
        if d2xx.FT_Write(self.handle, buffer, bytes, byref(count)) == OK:
            return count.value
        raise D2XXException

誰かがさまざまなオプションについてhttp://tungwaiyip.info/blog/2009/07/16/ctype_performance_benchmark [一部のベンチマーク]を行いました。

C ++ライブラリを多くのクラス/テンプレート/などでラップしなければならなかった場合、私はもっとためらうかもしれません。 ただし、ctypesは構造体でうまく機能し、http://docs.python.org/library/ctypes.html#callback-functions [callback]を使用してPythonに組み込むこともできます。


95


Cythonはそれ自体が非常にクールなツールであり、学ぶ価値があり、驚くほどPython構文に近いです。 Numpyで科学的な計算を行う場合、Cythonは高速な行列演算のためにNumpyと統合されるため、進むべき道です。

CythonはPython言語のスーパーセットです。 任意の有効なPythonファイルを投げることができ、有効なCプログラムを吐き出します。 この場合、CythonはPython呼び出しを基礎となるCPython APIにマップするだけです。 これにより、コードが解釈されなくなるため、おそらく50%高速化されます。

最適化を行うには、Cythonに型宣言などのコードに関する追加の事実を伝える必要があります。 十分に言えば、コードを純粋なCに要約できます。 つまり、PythonのforループはCのforループになります。 ここでは、大幅な速度の向上が見られます。 ここから外部Cプログラムにリンクすることもできます。

Cythonコードの使用も非常に簡単です。 私はマニュアルが難しく聞こえると思いました。 あなたは文字通りちょうどします:

$ cython mymodule.pyx
$ gcc [some arguments here] mymodule.c -o mymodule.so

そして、Pythonコードで「mymodule」をインポートし、Cにコンパイルされることを完全に忘れることができます。

いずれにせよ、Cythonはセットアップと使用を開始するのが非常に簡単なので、Cythonがニーズに合っているかどうかを確認することをお勧めします。 探しているツールではないことが判明しても無駄にはなりません。


40


PythonアプリケーションからCライブラリを呼び出すには、http://cffi.readthedocs.org/en/latest/ [* cffi *]もあります。これは、_ctypes_の新しい代替手段です。 FFIに新鮮な外観をもたらします。

  • それは魅力的できれいな方法で問題を処理します ctypes

  • 非Pythonコードを記述する必要はありません(SWIG、Cython、 …​)


20


私はそこに別のものを投げます:http://www.swig.org/Doc1.3/Python.html[SWIG]

学習は簡単で、多くのことを正しく行い、さらに多くの言語をサポートしているため、学習に費やした時間は非常に役立ちます。

SWIGを使用する場合、新しいPython拡張モジュールを作成しますが、SWIGを使用すると、ほとんどの面倒な作業を行うことができます。


18


個人的には、Cで拡張モジュールを作成します。 Python Cの拡張機能に怖がらないでください。書くのは難しくありません。 ドキュメントは非常に明確で有用です。 PythonでC拡張機能を初めて作成したとき、その作成方法を理解するのに約1時間かかりました。


10


ctypesは、コンパイル済みのライブラリBLOB(OSライブラリなど)を既に持っている場合に便利です。 ただし、呼び出しのオーバーヘッドは厳しいため、ライブラリに多くの呼び出しを行い、とにかくCコードを作成する(または少なくともコンパイルする)場合は、 cython。 それほど作業は必要ありません。結果のpydファイルを使用する方がはるかに高速で、よりPython的になります。

私は個人的にPythonコードの高速化のためにcythonを使用する傾向があります(ループと整数の比較はcythonが特に輝く2つの分野です)、そして他のライブラリのコード/ラッピングが関与している場合は、http:// www.boost.org/doc/libs/1_41_0/libs/python/doc/index.html[Boost.Python]。 Boost.Pythonはセットアップが難しい場合がありますが、一度動作させると、C / C ++コードのラッピングが簡単になります。

cythonは、http://numpy.scipy.org/ [numpy](http://conference.scipy.org/proceedings/SciPy2009/[SciPy 2009の議事録]から学んだ)のラッピングにも優れていますが、 tはnumpyを使用したので、コメントできません。


9


定義済みのAPIを備えたライブラリを既にお持ちの場合は、少し初期化するだけで、おおよそ以前と同じようにライブラリを呼び出すことができるため、 `ctypes`が最適なオプションだと思います。

CythonまたはCで拡張モジュールを作成すること(それほど難しくありません)は、新しいコードが必要な場合に便利です。 そのライブラリを呼び出して、複雑で時間のかかるタスクを実行し、結果をPythonに渡します。

単純なプログラムの別のアプローチは、異なるプロセスを直接実行し(外部でコンパイル)、結果を標準出力に出力し、サブプロセスモジュールで呼び出します。 時にはそれが最も簡単なアプローチです。

たとえば、そのように多かれ少なかれ動作するコンソールCプログラムを作成する場合

$miCcode 10
Result: 12345678

Pythonから呼び出すことができます

>>> import subprocess
>>> p = subprocess.Popen(['miCcode', '10'], shell=True, stdout=subprocess.PIPE)
>>> std_out, std_err = p.communicate()
>>> print std_out
Result: 12345678

少し文字列の書式設定を行うと、任意の方法で結果を取得できます。 標準エラー出力をキャプチャすることもできるため、非常に柔軟です。


6


cythonではなくctypesを使用するようにし、他の回答では言及されていない問題が1つあります。

ctypesを使用すると、結果は使用しているコンパイラにまったく依存しません。 ネイティブ共有ライブラリにコンパイルされる可能性のある多かれ少なかれ任意の言語を使用してライブラリを作成できます。 どのシステム、どの言語、どのコンパイラーであるかは重要ではありません。 ただし、Cythonはインフラストラクチャによって制限されています。 たとえば、Windowsでインテルコンパイラーを使用する場合、cythonを機能させるのははるかに難しいです。コンパイラーをcythonに「説明」し、この正確なコンパイラーで何かを再コンパイルする必要があります。 移植性が大幅に制限されます。


3


Windowsを対象とし、独自のC ライブラリをラップすることを選択した場合、異なるバージョンの `msvcrt ***。dll`(Visual C ランタイム)がわずかに互換性がないことがすぐにわかるかもしれません。

これは、結果の wrapper.pyd`が msvcr90.dll` (Python 2.7)_または msvcr100に対してリンクされているため、http://www.cython.org/ [ Cython`]を使用できない可能性があることを意味します。 dll` _(Python 3.x)。 ラップしているライブラリが異なるバージョンのランタイムに対してリンクされている場合、運が悪いです。

次に、動作させるには、C ライブラリのCラッパーを作成し、C ライブラリと同じバージョンの msvcrt *。dll`に対してそのラッパーDLLをリンクする必要があります。 次に、http://docs.python.org/2/library/ctypes.html [`ctypes]を使用して、実行時に手巻きラッパーDLLを動的にロードします。

そのため、多くの小さな詳細があります。詳細については、次の記事で説明します。

「美しいネイティブライブラリ_(Pythonで)_」:http://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/