Cythonの使い方についての解説はあるが、型についての記事が少ないように感じたのでまとめる。
CythonはほぼPythonと変わらない書き方でCとPythonをインターフェースを作れる、更にPythonの高速化が望める言語である。しかし、CとPythonの型が混在することで型エラーが頻発する。私はCythonの難しさは型のコントロールであると思った。
#静的型付けと動的型付けの融合
CythonはPythonのような動的型付けもできるし、Cのような静的な型付けができるという言語だ。Pythonで動的型付けと静的型付けの強みを享受できる便利な機能であるが、CythonではPythonの型だけでなくCの型が混在するため、注意しなければならないポイントが多い。
Cythonを使用する時に、肝に銘じておいて欲しいのは**Cythonは型を明示的に型付けしていなければ、それはPythonの型として扱われる。(実体がPythonレベルの型でない場合、Cレベルの型にキャストされる)**ということである。
ちなみに、Cythonではvector<int>
をvector[int]
と書く。
cdef vector[int] vec1 # Cレベルの型
Cythonではcdef
を使うことで変数をCレベルの型で変数宣言できる。なお、このcdef
はdocstringなどのコメントを除いて、必ず関数の最初に書かなければいけないという制約がある。つまり以下のようにif文
で分岐によってcdef
する型を変えることはできない。
#このような書き方はできない
cdef func0(type):
if type == "double":
cdef double x = 10
return x
elif type == "int":
cdef double y = 10
return y
Cythonでは返り値も明示的に指定できる。この型指定はPythonの型を返り値とする場合任意だが、Cレベルの型を返り値とする場合は型を指定しなければならない。もし、指定しなかった場合、Pythonレベルの型に変換される。
cdef func1(type):
cdef vector[double] vec
return vec #各要素がfloatのリストに変換される
cdef vector[double] func2(type):#返り値の型を指定
cdef vector[double] vec
return vec #vector[double]のまま返る
CythonにはPythonレベルの型とCレベルの型を暗黙のうちに型変換してくれる機能がある。例えばでvector[double]を返り値の型として指定しなくても、自動的にCythonがvector<double>
のvector
をlist
に、double
をfloat
に変換する。
返り値としたい型がユーザー定義型やCythonで扱えないCのライブラリ固有の型である場合、Pythonレベルへの変換が定義されていないためコンパイルエラーが起きる。
以下の例ではcdef
関数内からMyClassというCで定義した型を返している。このMyClass型はPythonの型への変換方法が定義されていないので、以下の例ではエラーが起きる。
cdef func3():
cdef MyClass x = MyClass()
return x # エラー MyClass型はPythonの型に変換できない
func3()
このため、Cレベルの型の値を返したい場合は、以下のように返り値の型を指定する必要がある。
cdef MyClass func3():
cdef MyClass x = MyClass()
return x # OK 返り値はMyClassである
func3()
これはMyClassを要素とするvector
でも同様である。
#型キャストとオーバーロード
CythonはPythonを高速化できるだけでなく、Cの関数や型をラップできるのが大きな魅力だ。自分もラップをしようとしたがCでオーバーロードしている関数をラップしたい場合にかなりてこずった。
cdef extern from "rect.h":
int area(int x,int y)
double area(double x,double y)
例えば上のような関数があるとしよう。これをxの型によって、呼び出す関数を変えたいので以下のように書く。するとエラーが起きる。
#エラー 適切なメソッドを呼び出せない
def py_area (x,y):
if type(x) == int:
return area(x,y)
elif type(x) == float:
return area(x,y)
このような書き方をしたい場合、実引数として渡す値を全て適切な型に明示的キャストしてから渡す必要がある。つまり型キャストをその場で明示的な型付けを行うのに使うということになる。
#OK
def py_area (x,y):
if type(x) == int:
return area(<int>x,<int>y)
elif type(x) == float:
return area(<double>x,<double>y)
しかし、仮引数が参照渡しで宣言されているときは仮引数をその場でキャストしながら渡すことができない。この時、予めcdef
で型宣言しておくことで明示的な型指定しながら渡すことができる。
例えば関数が下のようになっているなら
cdef extern from "rect.h":
int area(int& x,int& y)
double area(double& x,double& y)
今までの書き方を下のような書き方にする必要がある。
def py_area (x,y):
cdef:
int x1
int y1
double x2
double y2
if type(x) == int:
x1 = x
y1 = y
return area(x1,y1)
elif type(x) == float:
x2 = x
y2 = y
return area(x2,y2)
すると、参照型で仮引数が定義されていてもエラーにならない。
#Fused type
CythonにはFused type(融合型)という機能がある。これはCythonで実質的にテンプレート型を使う機能である。返り値や引数に複数の型がありうる場合に使える。
#任意の型を羅列
ctypedef fused my_type:
hoge
foo
bar
Fused type
は上記のように型を羅列することでmy_type
はhoge
、foo
、bar
のどの型としても扱われるようになる。
これを使うことで、多次元リストのCとPython間の型変換を以下のように実現できる。PyClass
はMyClass
のPython上での型とする。
ctypedef fused T:
MyClass
vector[MyClass]
cdef vector_to_list (T x):
if T == vector[MyClass]:
return [vector_to_list(i) for i in range(<vector[MyClass]>x.size())]
else :
return PyClass(x)
Fused typeは全ての型である可能性を考慮して構文解析する。例えば上の例ならCythonのxはvector[MyClass]
だけでなく、MyClass
の可能性があると見なしてしまう。キャストによって明示的な型指定をせず下のように書くとMyClassにはsize()
がないのでエラーを起こす。
ctypedef fused T:
MyClass
vector[MyClass]
cdef vector_to_list (T x):
if T == vector[MyClass]:
return [vector_to_list(i) for i in range(x.size())] # (1)
else :
return PyClass(x)
この例では、(1)の行を<vector[MyClass]>
でキャストしていないため型T
がMyClassの可能性があると見なされる。そして、MyClassにはsize()が定義されていないというエラーが起きる。
#まとめ
CythonはCとPythonが融合している言語故に、二つの特性を活用できるが面倒ごとが多く感じた。
とりあえず、Cythonの型に困ったら、cdef
による静的型付けやキャストをうまく使って切り抜けよう。
#参考文献
- Kurt W. Smith 著、中田 秀基 監訳、長尾 高弘 訳 「Cython - Cとの融合によるPythonの高速化」2015
- Welcome to Cython’s Documentation — Cython 3.0a0 documentation