この記事は「深入りしないCython入門」の続きです。
今回もあまり深入りしないようにCythonに入門していこう。
なお、この記事はあくまで深入りせずに、楽してCythonのおいしい部分を頂くことを目的としている。
Cython記法早めぐり
Cythonの記法はそれほど難しくない、Cythonのチュートリアルに良いサンプルコードがあったので、それを拝借しよう。
def myfunction(x, y=2):
a = x - y
return a + x * y
def _helper(a):
return a + 1
class A:
def __init__(self, b=0):
self.a = 3
self.b = b
self._scale = 2.0
self.read_only = 1.0
def foo(self, x):
return x + _helper(1.0)
上のコードをCythonで最適化するとこうなる。
%%cython
cpdef int myfunction(int x, int y=2):
cdef int a = x - y
return a + x * y
cdef double _helper(double a):
return a + 1
cdef class A:
cdef public int x
cdef public int y
cdef double _scale
cdef readonly float read_only
def __init__(self, int b=0):
self.a = 3
self.b = b
self._scale = 2.0
self.read_only = 1.0
cpdef double foo(self, double x):
return (x + _helper(1.0)) * self._scale
変数、引数、戻り値の型指定の方法は見ればわかるので、説明するまでもないだろう。
関数宣言
関数宣言をよく見ると、myfunction関数はcpdef
、_helper関数はcdef
で定義されている、関数宣言の一覧は以下の通り。
宣言 | 説明 |
---|---|
def | 低速、Pythonから呼び出す事ができる |
cdef | 高速、Pythonからは呼び出しできない、Cython内のみ使用可 |
cpdef | defとcdefのハイブリッド、Pythonから呼ばれる場合def、Cythonから呼ばれる場合はcdefで呼び出される |
cdefクラス
クラス宣言でcdef class A
とすると、cdefクラスとなる。
cdefクラスは、普通のクラスはdictでアトリビュートを管理しているのに比べ、構造体で管理しているため、メモリ効率もよくアクセスも高速であるが、以下のような制限を受ける。
- 動的なメソッド/メンバの追加不可
- cdefメソッドを親とした多重継承不可、単一継承は可
メンバの定義は以下のように事前に定義しなければならない。
cdef class A:
cdef public int x
cdef public int y
cdef double _scale
cdef readonly float read_only
...
_scale
メンバのようにpublicを付けないとPythonからの参照は不可である。
また、read_only
メンバのようにreadonly
属性をつけるとPythonからの変更が不可となる。
a = A()
a._scale # エラー
a.read_only = 2.0 # エラー
Cythonで使うファイル拡張子一覧
拡張子 | 説明 |
---|---|
.pyx | 実装ファイル、プログラム本体と考えればよい |
.pxd | 定義ファイル |
.pxi | インクルードファイル |
以上のことを知っていれば、だいたい困らないであろう。
pure Pythonモード
実際にPythonプログラムの高速化手順としては、オリジナルのプログラムに型定義を書き加えていくというのが一般的な手順である。
そこで、あえて違うアプローチを提案しよう、それが「pure Pythonモード」である。
上記のサンプルをpure Pythonモードで書き直してみよう。
%%cython
import cython
@cython.ccall
@cython.locals(x=cython.int, y=cython.int)
@cython.returns(cython.int)
def myfunction(x, y=2):
a = x-y
return a + x * y
@cython.cfunc
@cython.locals(a=cython.double)
@cython.returns(cython.double)
def _helper(a):
return a + 1
@cython.cclass
class A:
a = cython.declare(cython.int, visibility='public')
b = cython.declare(cython.int, visibility='public')
_scale = cython.declare(cython.double)
read_only = cython.declare(cython.double, visibility="readonly")
@cython.locals(b=cython.int)
def __init__(self, b=0):
self.a = 3
self.b = b
self._scale = 2.0
self.read_only = 1.0
@cython.ccall
@cython.locals(x=cython.double)
@cython.returns(cython.double)
def foo(self, x):
return x + _helper(1.0) * self._scale
Pythonコードにimport cython
して、ひたすらデコレートで型情報を追加していくスタイルである。これで、同一のファイルでPythonでの実行と、Cythonでのコンパイルを兼ねる事ができる。
関数外に型情報を定義するため、関数内のPythonコード部分は一切変更しなくて良い。
処理の部分の可読性はそのままなので、デコレータの嵐に見慣れれば意外と快適である。
Cythonのpure Pythonモードの詳細は公式ドキュメント見てほしい、簡潔なチュートリアルなので英文は殆ど無いので読みやすい。
補助(agumenting).pxdファイル
pure Pythonモードで関数内のコードをそのままに高速化ができるが、.pxdファイルを使うと.pyファイルをまるごと変更せずに高速化することができる。
この説明は公式マニュアルに簡潔な説明があるので、一部抜粋する。
.py ファイルと同名の .pxd が見つかると、 Cython は cdef されたクラスや、 cdef/cpdef され た関数やメソッドを走査します。次に、 .py 中の対応するクラス・ 関数・メソッドを、適切な型に変換します。 従って、もし下記のような a.pxd があったとして、:
cdef class A:
cpdef foo(self, int i)
同時に以下のような a.py というファイルがあると、:
class A:
def foo(self, i):
print "Big" if i > 1000 else "Small"
コードは下記のように解釈されます:
cdef class A:
cpdef foo(self, int i):
print "Big" if i > 1000 else "Small"
タイプヒントとCythonの連携(希望的観測)
pure Pythonモードのコードをよく見るとPyCharmのタイプヒンティングに似ている。
私はPyCharmのタイプヒントをよく使うので、pure Pythonモードは使いやすく感じた。
class A:
"""
:type a: int
:type b: int
"""
def __init__(self, b=0):
"""
:type b: int
"""
self.a = 3
self.b = b
def foo(self, x):
"""
:type x: float
:rtype: float
"""
return x * float(self.a)
また、Pythonには型情報のみを書いたスタブファイル(.pyi)というものがる。
スタブファイルは最後に説明した「補助(agumenting).pxdファイル」に非常によく似ている。
将来的にはタイプヒントを書いたPythonコード、または型アノテーションを書いたPythonコードは、コードに手をいれずとも自動でCythonによる高速化がされる(もちろん完全な高速化は難しいだろうが)ようになると個人的には嬉しい。
しかし、私の調べた限りではそのような情報は見つからなかった、タイプヒントとCython自動化について情報を持っている方は是非ご教授いただきたい。
まとめ
かなり早足だが、Cythonの機能を紹介した。
Cythonの機能はまだまだ沢山あるが、とりあえずCythonを使って高速化するには充分だろう。
更にCythonの概要を知りたいのであれば、下記のスライドがよくまとまっているのでオススメである。