深入りしないCython入門 -2-

  • 37
    Like
  • 0
    Comment

この記事は「深入りしない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最適化コード
%%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モードで書き直してみよう。

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モードの詳細は公式ドキュメント見てほしい、簡潔なチュートリアルなので英文は殆ど無いので読みやすい。

http://cython.readthedocs.io/en/latest/src/tutorial/pure.html

補助(agumenting).pxdファイル

pure Pythonモードで関数内のコードをそのままに高速化ができるが、.pxdファイルを使うと.pyファイルをまるごと変更せずに高速化することができる。

この説明は公式マニュアルに簡潔な説明があるので、一部抜粋する。

http://omake.accense.com/static/doc-ja/cython/src/tutorial/pure.html

.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モードは使いやすく感じた。

PyCharmのタイプヒンティングの例
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)というものがる。

https://www.python.org/dev/peps/pep-0484/#stub-files

スタブファイルは最後に説明した「補助(agumenting).pxdファイル」に非常によく似ている。

将来的にはタイプヒントを書いたPythonコード、または型アノテーションを書いたPythonコードは、コードに手をいれずとも自動でCythonによる高速化がされる(もちろん完全な高速化は難しいだろうが)ようになると個人的には嬉しい。

しかし、私の調べた限りではそのような情報は見つからなかった、タイプヒントとCython自動化について情報を持っている方は是非ご教授いただきたい。

まとめ

かなり早足だが、Cythonの機能を紹介した。
Cythonの機能はまだまだ沢山あるが、とりあえずCythonを使って高速化するには充分だろう。

更にCythonの概要を知りたいのであれば、下記のスライドがよくまとまっているのでオススメである。

http://www.behnel.de/cython200910/talk.html