拡張型

はじめに

Cython では、 class 文を使った通常の Python クラスの他に、 新たな Python のビルトイン型、いわゆる拡張型クラス (extension type class) も定義できます。拡張型は cdef class クラス文で定義し ます。:

cdef class Shrubbery:

    cdef int width, height

    def __init__(self, w, h):
        self.width = w
        self.height = h

    def describe(self):
        print "This shrubbery is", self.width, \
            "by", self.height, "cubits."

ご覧のように、Cython の拡張型の定義は Python のクラス定義とよく似てい ます。クラスの中では、 def 文を使って、 Python のコードから呼び出せる メソッドを定義できます。また、 Python のクラスと同様、 __init__() のような特殊メソッドの多くも定義できます。

大きな違いは、 cdef 文を使ってアトリビュートを定義できると いう点です。アトリビュートは Python のオブジェクト (一般のオブジェクト 型でも、何らかの拡張型でも) や、何らかの C のデータ型にできます。その ため、拡張型を使うと、任意の C データ構造をラップしながら、Python から は Python ライクなインタフェースでアクセスできます。

アトリビュート

拡張型のアトリビュートは、オブジェクトの C 構造体に直接保存されます。 拡張型にどのようなアトリビュートをもたせるかは、コンパイル時に決定して いなければなりません。通常の Python のオブジェクトのアトリビュートと違っ て、拡張型には、実行時に代入などでアトリビュートを追加できません。 (ただし、Python で拡張型のサブクラスを作れば、実行時にアトリビュートを 追加できます。)

拡張型のアトリビュートには、二つの方法でアクセスできます。一つは Python のアトリビュートルックアップで、Python ではこの方法しか使えませ ん。もう一つは、Cython のコードから C の構造体に直接アクセスするもので、 Cython からは上のルックアップとこの方法の両方を使えます。

拡張型のアトリビュートには、デフォルトの状態では直接アクセスしかできず、 Python のコードからはアクセスできなくなっています。 アトリビュートを Python からアクセスできるようにするには、アトリビュー トを public または readonly で宣言せねばなりませ ん:

cdef class Shrubbery:
    cdef public int width, height
    cdef readonly float depth

この例では、 widthheight アトリビュートは Python のコード から読み書きできます。 depth アトリビュートは Python からは読み出 しのみ可能で、変更はできません。

Note

Python からアクセス可能なアトリビュートにできるのは、int, float, string といった単純な C の型だけです。その他には、Python の値を アトリビュートとして公開できます。

Note

publicreadonly といったオプションは Python からのアクセスにのみ影響を及ぼします。拡張型のアトリビュー トは全て、 Cレベルで読み書きアクセスできます。

型宣言

拡張型のアトリビュートに直接アクセスするには、まず、あるオブジェクトが 拡張型のオブジェクトであって、ただの一般の Python オブジェクトでないこ とを Cython に知らせねばなりません。Cython は、拡張型のメソッドの self パラメタについてはすでに承知していますが、それ以外の場合には、 明に型宣言してやる必要があります。

例えば、以下のような関数を考えましょう:

cdef widen_shrubbery(sh, extra_width): # BAD
    sh.width = sh.width + extra_width

sh パラメタには型宣言がないので、width アトリビュートは Python の アトリビュートとしてルックアップされます。アトリビュートを publicreadonly で宣言していれば、この操作は 一応動作しますが、きわめて非効率的でしょう。アトリビュートがプライベー トであれば、全く動作しません – コードはコンパイルできますが、実行時に はアトリビュートエラーを送出するでしょう。

解決するには、以下のようにして、 shShrubbery 型として 宣言します:

cdef widen_shrubbery(Shrubbery sh, extra_width):
    sh.width = sh.width + extra_width

これで、 Cython コンパイラには、 shwidth という C のア トリビュートを持つことがわかるので、アトリビュートに直接かつ効率的に アクセスするコードを生成します。 ローカル変数についても、以下のように、同じ挙動があてはまります:

cdef Shrubbery another_shrubbery(Shrubbery sh1):
    cdef Shrubbery sh2
    sh2 = Shrubbery()
    sh2.width = sh1.width
    sh2.height = sh1.height
    return sh2

型のテストとキャスト

Shrubbery 型のオブジェクトを返す quest() というメソッド があったとしましょう。戻り値オブジェクトの width にアクセスするには、 以下のように書けます:

cdef Shrubbery sh = quest()
print sh.width

このコードにはローカル変数が必要で、その中では代入時に型のテストを行なっ ています。 quest() の戻り値が Shrubbery だと 知っていれば 、キャストを使って以下のように書けます:

print (<Shrubbery>quest()).width

ただし、この操作には、 quest() が実際には Shrubbery でない場合に、存在しない C の構造体メンバ width にアクセスしようと試み るという危険性があります。 C のレベルでは、 AttributeError を 送出するのではなく、意味のない値 (該当アドレスにある何らかのデータを無 理やり int として解釈した結果) を返したり、無効なメモリ領域にアクセス を試みて、segfault を起こしたりします。代わりに、以下のような書き方:

print (<Shrubbery?>quest()).width

をすると、キャストする前に型チェックをして (場合によって TypeError を送出して) から、処理を進めます。

オブジェクトの型を明示的にテストするには、 isinstance() メソッド を使ってください。Python のデフォルトでは、 isinstance() メソッ ドは、第一引数の __class__ アトリビュートを調べて、引数が目的 の型かどうかを判定しますが、 __class__ アトリビュートは偽装し たり変更したりできるので、安全ではないことがあります。しかし、 cdef アトリビュートにアクセスしたり、 cdef メソッ ドを呼び出したりするには、拡張型の C の構造体は正しくなくてはなりませ ん。そこで、 Cython は、第二引数に既知の拡張型が指定されているかどうか 調べ、 Pyrex’s typecheck() に似た型チェックを行います。古いタイ プの型チェックの挙動は、第二引数をタプルで渡せば使えます。:

print isinstance(sh, Shrubbery)     # sh の型をチェック
print isinstance(sh, (Shrubbery,))  # sh.__class__ の値をチェック

拡張型と None

パラメタや C の変数を拡張型で宣言すると、 Cython はその値が拡張型オブ ジェクトの他に None も取りうるとみなします。 これは C のポインタが NULL を値に取れるのに似ていて、それがゆえに 同じような注意が必要です。拡張型に対して Python の操作を行なっている間 は、完全な動的型チェックが行われるため、何の問題もありません。しかし、 拡張型の C アトリビュートに (上の widen_shrubbery のように) アクセスす る場合には、オブジェクトが None でないかどうかを自分で調べねばなり ません – 効率重視のため、 Cython はチェックを行わないのです。

拡張型を引数に取る Python の関数を公開するときは、特に注意が必要です。 例えば、 widen_shrubbery() を Python の関数にする場合、以下の様 な書き方をすると:

def widen_shrubbery(Shrubbery sh, extra_width): # これは
    sh.width = sh.width + extra_width           # 危険な書き方!

モジュールのユーザは shNone を渡した時にクラッシュしてしま います。

一つの回避策は、以下のように書くことです:

def widen_shrubbery(Shrubbery sh, extra_width):
    if sh is None:
        raise TypeError
    sh.width = sh.width + extra_width

しかし、このような書き方を頻繁にせねばらないと容易に想像できるので、 Cython にはもっと便利な方法があります。 Python の関数に拡張型のパラメ タを宣言するときに、 not None 節を付加できるのです:

def widen_shrubbery(Shrubbery sh not None, extra_width):
    sh.width = sh.width + extra_width

これで、この関数は、引数が正しい型であるかどうかのチェックと同時に、 shnot None でないかも自動的にチェックします。

Note

not None 節を使えるのは、 Python の関数 (def で定義 したもの) だけで、 C の関数 (cdef で定義したもの) では 使えません。C の関数のパラメタの値が None かどうかをチェックしたけ れば、自分で行う必要があります。

Note

その他にも、いくつか注意点があります:

  • 拡張型のメソッドの self パラメタは、必ず None でないことが保 証されています。
  • 値を None と比較するときに注意せねばならないのは、 x が Python のオブジェクトのとき、 x is Nonex is not None は C のポインタ比較に翻訳されるためにとても効率的であるというこ とです。一方、 x == Nonex != None、 あるいは単に x のブール値評価 (if x: ... のような書き方) は、Python の演算を呼び出すため、かなり低速になります。

特殊メソッド

拡張型における __xxx__() 形式の特殊メソッドの多くと Python の同 様のメソッドは、基本的な原理を同じくするものの、明確な違いがあります。 特別に、 この話題のためのページ を割いてある ので、拡張型に特殊メソッドを実装する前に注意深く目を通しておいて下さい。

プロパティ

拡張型クラスの中で、プロパティを以下のような特殊な構文で宣言します:

cdef class Spam:

    property cheese:

        "プロパティの docstring はここに書く"

        def __get__(self):
            # プロパティの値を読むときに呼び出される
            ...

        def __set__(self, value):
            # プロパティの値を変更するときに呼び出される
            ...

        def __del__(self):
            # プロパティの値を削除するときに呼び出される

__get__(), __set__(), __del__() メソッドはそれ ぞれ省略できます。省略すると、各メソッドに対応する操作を行った時に例外 を送出します。

プロパティを定義したクラスの例を以下に示します。このクラスでは、プロパ ティに値を入れる (__set__()) と、その値をリストに追加します。値 を参照する (__get__()) と、リスト全体を返します。値を削除する (__del__()) と、リストを空にします:

# cheesy.pyx
cdef class CheeseShop:

    cdef object cheeses

    def __cinit__(self):
        self.cheeses = []

    property cheese:

        def __get__(self):
            return "We don't have: %s" % self.cheeses

        def __set__(self, value):
            self.cheeses.append(value)

        def __del__(self):
            del self.cheeses[:]

# テスト入力
from cheesy import CheeseShop

shop = CheeseShop()
print shop.cheese

shop.cheese = "camembert"
print shop.cheese

shop.cheese = "cheddar"
print shop.cheese

del shop.cheese
print shop.cheese
# テスト出力
We don't have: []
We don't have: ['camembert']
We don't have: ['camembert', 'cheddar']
We don't have: []

サブクラス化

拡張型は、ビルトイン型や、他の拡張型を継承できます:

cdef class Parrot:
    ...

cdef class Norwegian(Parrot):
    ...

型を継承する場合、基底型の完全な定義を Cython に伝えねばなりません。そ のため、基底型がビルトイン型の場合、事前に extern 拡張型として宣言 されていなければなりません。基底型が別の Cython モジュールで宣言されて いる場合、 extern 拡張型で宣言されているか、 cimport 文 で import せねばなりません。

拡張型は、基底クラスを一つしか持てません (多重継承は行えません)。

Cython の拡張型を Python でサブクラス化することも可能です。複数の拡張 型を継承する Python のクラスも作れますが、その場合には、(全ての基底ク ラスの C データ型のレイアウトに互換性がなければならないなど) 多重継承 に対する Python のルールに従わねばなりません。

Cython 0.13.1 から、拡張型を Python でサブクラス化させない方法ができま した。これは、 final ディレクティブを使ってでき、たいていは拡張型 に対してデコレータを使ってセットします:

cimport cython

@cython.final
cdef class Parrot:
   def done(self): pass

final で宣言した Python のサブクラスをサブクラス化しようとすると、 実行時に TypeError を送出します。また、 Cython でも、同じモジュ ールの final 型のインスタンスからのサブタイプ化を禁止します。すなわち、 final 型を基底型にもつような拡張型を定義しようとすると、コンパイル時に 失敗します。ただし、現状、この制約は他の拡張モジュールに伝播しないので、 final で宣言した拡張型を、他のコードから C レベルでサブタイプ化できて しまいます。

C のメソッド

拡張型には、 Python のメソッドの他に C のメソッドも持たせられます。 C の関数と同様、 C のメソッドの定義には、 def の代わりに cdefcpdef を使います。 C のメソッドは「仮想関数 (“virtual”)」であり、拡張型から導出した別の拡 張型でオーバライドできます:

# pets.pyx
cdef class Parrot:

    cdef void describe(self):
        print "This parrot is resting."

cdef class Norwegian(Parrot):

    cdef void describe(self):
        Parrot.describe(self)
        print "Lovely plumage!"


cdef Parrot p1, p2
p1 = Parrot()
p2 = Norwegian()
print "p1:"
p1.describe()
print "p2:"
p2.describe()
# 出力
p1:
This parrot is resting.
p2:
This parrot is resting.
Lovely plumage!

上の例では、 C メソッドが通常の Python と同じテクニックを使って、基底 型から継承した C メソッドを呼ぶ様子も、すなわち:

Parrot.describe(self)

を実演しています。

拡張型の前方宣言

拡張型は、 structunion のように前方宣言 (forward declaration) できます。前方宣言は、以下の例のように、二つの拡 張型が存在して、お互いを参照している場合に必要です:

cdef class Shrubbery # forward declaration

cdef class Shrubber:
    cdef Shrubbery work_in_progress

cdef class Shrubbery:
    cdef Shrubber creator

基底クラスを持つ拡張型を前方宣言する場合、前方宣言とクラス定義の両方で 基底クラスを指定せねばなりません:

cdef class A(B)

...

cdef class A(B):
    # アトリビュートやメソッド

拡張型を弱参照可能にする

デフォルトの状態では、拡張型に対する弱参照は作成できません。 弱参照を有効にするには、 object 型の C アトリビュート __weakref__ を宣言します:

cdef class ExplodingAnimal:
    """この animal は、強い参照がなくなったら自動的に
    削除される"""

    cdef object __weakref__

public および extern 拡張型

拡張型は extern や public で宣言できます。 extern で拡張型を宣言すると、 外部の C コードで定義した拡張型を Cython モジュールで利用できます。 public で拡張型を宣言すると、 Cython モジュールで定義した拡張型を外部 の C コードで利用できます。

extern 拡張型

extern 拡張型を使うと、 Python のコアシステムで定義されている Python オブジェクトの内部構造にアクセスしたり、 Cython を使わない拡張モジュー ルから拡張型にアクセスしたりできます。

Note

Pyrex の古いバージョンでは、 extern 拡張型は、他の Pyrex モジュー ルで定義されている拡張型の参照に使われていました。同じことは今でも できますが、 Cython はよりよいメカニズムを備えています。 Cython モジュール間で宣言を共有する を参照してください。

ビルトインの複素数オブジェクトの C レベルのメンバを取り出す例を示しま す:

cdef extern from "complexobject.h":

    struct Py_complex:
        double real
        double imag

    ctypedef class __builtin__.complex [object PyComplexObject]:
        cdef Py_complex cval

# 上の型を使う関数
def spam(complex c):
    print "Real:", c.cval.real
    print "Imag:", c.cval.imag

Note

いくつか重要な点があります:

  1. この例では ctypedef クラスを使っています。 これは、 Python のヘッダファイルで PyComplexObject 構造体を 以下のように宣言しているためです:

    typedef struct {
        ...
    } PyComplexObject;
    
  2. 拡張型の名前の他に、Python の型オブジェクトが入っているモジュー ルも指定しています。これについては、 暗黙の import の節を参照してください。

  3. extern 拡張型を宣言する際は、メソッドを宣言しないでください。 extern 拡張型のメソッドは Python のメソッドなので、宣言しなくて も呼び出せます。また、 structunion と 同様、拡張クラスの宣言が cdef extern from ブロックの中にあ る場合には、アクセスしたい C のメンバだけを宣言してください。

型の名前宣言のための節

クラス宣言の一部を角カッコで囲う書き方は、 extern および public 拡張型 だけで使える特殊な機能です。この部分の完全な形式は、以下のとおりです:

[object object_struct_name, type type_object_name ]

ここで、 object_struct_name は、拡張型の C 構造体に割り当てる名前、 type_object_name は拡張型の静的に宣言された type オブジェクトに割 り当てる名前です。(object 節と type 節は、どちらの順番でも書けます)。

拡張型宣言を cdef extern from ブロックの中で行う場合は、Cython が ヘッダファイルの宣言と互換性のあるコードを生成しなければならないため、 object 節が必須です。それ以外の場合には、 extern 拡張型では object 節 は不要です。

public 拡張型の場合、Cython が外部の C コードと互換性のあるコードを生 成しなければならないので、 object 節と type 節の両方が必要です。

暗黙の import

Cython では、 extren 拡張クラスの宣言をする際に、以下の例のように、モ ジュール名も含めねばなりません:

cdef extern class MyModule.Spam:
    ...

型オブジェクトは指定したモジュールから暗黙のうちに import され、指定の 名前でこのモジュールに束縛されます。言い換えると、この例では、暗黙のう ちに:

from MyModule import Spam

文をモジュールロード時に実行したのと同じです。

パッケージ階層の中に入っているモジュールを参照するときは、以下のように ドット名表記を使えます:

cdef extern class My.Nested.Package.Spam:
    ...

import した型に別の名前をつけたければ、以下のように as 節を使います:

cdef extern class My.Nested.Package.Spam as Yummy:
   ...

上の例は、以下の import 文に対応します:

from My.Nested.Package import Spam as Yummy

型の名前とコンストラクタの名前

Cython モジュール中では、拡張型の名前には二つの異なる用途があります。 式の中で使うと、拡張型のコンストラクタ (すなわち、型オブジェクト) を保 持するモジュールレベルでグローバルな変数を表します。一方、拡張型の名前 は、変数や引数、戻り値の宣言に使う C の型の名前にも使えます。

以下のように宣言すると:

cdef extern class MyModule.Spam:
    ...

Spam という名前は、前述の二つの役割を持つようになります。コンスト ラクタの参照には、他の名前を使えますが、型宣言に使えるのは、 Spam だけです。例えば、明示的に MyModule を import した場合には、 MyModule.Spam()Spam インスタンスを生成できますが、 MyModule.Spam を型の宣言には使えません。

as 節を使うと、 as で指定した名前に両方の役割が 引き継がれます。従って、下記のように宣言すると:

cdef extern class MyModule.Spam as Yummy:
    ...

Yummy は型の名前であると同時に、コンストラクタにもなります。もちろ んこの場合も、コンストラクタとしては、別の名前で保持できますが、型の名 前として使えるのは Yummy だけです。

public 拡張型

拡張型は public で宣言できます。 public で宣言すると、Cython は .h ファイルを生成して、その中にオブジェクトの構造体と型オブジェクトの宣言 を入れます。 .h ファイルを外部の C コードから include すると、拡張 型のアトリビュートとしてコードにアクセスできます。