unicode と文字列の扱い

Python 3 における文字列のセマンティクスと同じく、 Cython もバイト文字 列と unicode 文字列を厳密に区別します。このことは、とりもなおさず、 (Python 2 での文字列操作を除き) バイト文字列と unicode 文字列との間で 自動的な変換が行われないということです。文字列のエンコーディング・デコー ディングは全て、明にエンコーディング・デコーディングの過程を経ねばなり ません。

とはいえ、バイト文字列を C コードと Python の間で受け渡すのはとても簡 単です。C ライブラリからバイト文字列を受け取るとき、 Python の変数に代 入するだけで、 Python のバイト文字列への変換を Cython に委ねられます:

cdef char* c_string = c_call_returning_a_c_string()
cdef bytes py_string = c_string

上の例では、 Python のバイト文字列オブジェクトが、もとの C 文字列のコ ピーを保持します。このオブジェクトは Python コード中で安全に受け渡しで き、オブジェクトへの最後のリファレンスがスコープの外に出てしまえば、ガ ベージコレクションされます。大事なのは、C での常識と同じく、文字列中の ヌルバイトは終端文字列のように振舞うということです。そのため、上の例は ヌルバイトを含まない文字列に対してしか正しく動作しません。

ヌルバイトに対応していないことはさておき、長い文字列に対して上のやり方 は非効率です。というのも、 Cython は C の文字列に対してまず strlen() を呼び出して長さを調べ、ヌルバイトに到達するまでバ イトデータを一文字づつカウントせねばならないからです。多くの場合、例え ば関数が文字列の長さを返すようになっているなどの理由で、ユーザコードは 文字列の長さを把握しているからです。そのような場合、 文字列が何バイト の長さなのか、スライスで Cython に教えた方がはるかに高速です:

cdef char* c_string = NULL
cdef Py_ssize_t length = 0

# get pointer and length from a C function
get_a_c_string(&c_string, &length)

py_bytes_string = c_string[:length]

こうすると、バイト長のカウントは不要で、かつ文字列中にヌルバイトがいく つ入っているかに関わらず c_string 中の length バイトが Python のバイト列オブジェクトにコピーされます。上のケースでは、スライスのイン デクスの値は正確であると仮定して境界チェックを行っていないので、スライ スインデクスが間違っていると、データを破損したりクラッシュしたりします。

Python のバイト文字列を生成する際、メモリが足りないなどの理由で例外を 送出して失敗する可能性があります。Python オブジェクトへの変換を行った 後に元の文字列を free() したければ、代入の部分を try-finally 構文で囲っておく必要があります:

cimport stdlib
cdef bytes py_string
cdef char* c_string = c_call_creating_a_new_c_string()
try:
    py_string = c_string
finally:
    stdlib.free(c_string)

バイト文字列から C の char* に変換するには、逆の代入を行いま す:

cdef char* other_c_string = py_string

これはとても高速な操作で、実行すると other_c_string は Python の文 字列オブジェクト内のバイト文字列バッファを指すようになります。バッファ は Python 文字列オブジェクトの寿命に結びついているので、オブジェクトが ガベージコレクションされると、ポインタの値は無効になります。従って、 件の char* を使う間は、オブジェクトへの参照を維持しておくこ とが肝心です。この操作は、文字列へのポインタをパラメタに取る C の関数 を呼び出す際のつなぎとして非常によく使われますが、C の関数側が受け取っ たポインタを後で使うために保存する場合は十分注意してください。 Python に文字列オブジェクトへのリファレンスを維持させる以外には、メモリ管理は 一切必要ありません。

“const” の扱い

多くの C ライブラリが、APIの中で文字列に const モディファイアを使 い、文字列を変更しないことや、戻り値の文字列をユーザが変更してはならな いことを宣言しています。例えば:

.. code-block:: c
typedef const char specialChar; int process_string(const char* s); const unsigned char* look_up_cached_string(const unsigned char* key);

Cython は現状 const モディファイアを言語レベルでサポートしていませ んが、必要な宣言をテキストレベルで生成するようにはできます。

一般に、外部の C 関数の引数では const モディファイアの有無には関係 がなく、 Cython の宣言 (.pxd ファイルなどでの宣言) では省略できます。 以下のような宣言を Cython で行なっても、C コンパイラは正しく動作するは ずです:

cdef extern from "someheader.h":
    int process_string(char* s)   # 注: API がルーズになっている

しかし、戻り値や、 typedef で定義した API 固有の型を使っている変数など では、 const の有無は重要で、正しく使わないとコンパイラが警告を発 します。そこで、 libc.string モジュール中の型定義を使って、以下の ように書きます:

from libc.string cimport const_char, const_uchar

cdef extern from "someheader.h":
    ctypedef const_char specialChar
    int process_string(const_char* s)
    const_uchar* look_up_cached_string(const_uchar* key)

注: たとえ API が関数の引数にしか const を使っていなくても、 const_char を使って適切な型に宣言しておいたほうがよいでしょ う。その方が、例えば将来 const のサポートを獲得したときなどの対応 を楽にするからです。

バイト列をデコードしてテキストにする

文字列中にバイナリデータが入っている場合しか扱わないなら、最初に解説 した C 文字列を受け渡しする方法で十分です。しかしながら、何らかのエン コードされた文字列を扱う際には、 C のバイト文字列を Python の unicode 文字列にデコードして受け取り、引き渡すときには unicode 文字列を C の バイト文字列にエンコードするのが王道です。

Python のバイト文字列オブジェクトに対して、 .decode() メソッドを呼 び出すと、 Unicode 文字列に変換されます:

ustring = byte_string.decode('UTF-8')

Cython を使うと、同じことを C の文字列で行えます。ただし、バイト文字列 の途中にヌルバイトを含んではなりません:

cdef char* some_c_string = c_call_returning_a_c_string()
ustring = some_c_string.decode('UTF-8')

もっと効率的にしたければ、長さの分かっている文字列について以下のように 扱います:

cdef char* c_string = NULL
cdef Py_ssize_t length = 0

# C 関数から ポインタと長さの情報を得る
get_a_c_string(&c_string, &length)

ustring = c_string[:length].decode('UTF-8')

ここでも、文字列にヌルバイトが入っているときと同じ扱いをします。例えば UCS-4 のようなエンコーディングのときは、各文字が 4 バイトで表現されて いて、その大半の値が 0 だからです。

また、スライスをするときに境界チェックをしないので、スライスインデクス が間違っていると、データを破損したりクラッシュしたりします。 ただし、 Cython 0.17 からは負のインデクスを扱うことができ、その場合に は strlen() を呼び出して文字列の長さを調べます。 言うまでもなく、この負のインデクスを使えるのは、0 で終端されていて、か つ、中にヌルバイトをふくまない文字列です。 UTF-8 や ISO-8859 でエンコー ドしたテキストが、そのような文字列のいい例です。文字列中に本当にヌル文 字が入っていないか疑わしい場合は、おそらく大丈夫だとあてにせず、「明ら かに」正しいインデクスを渡すべきでしょう。

文字列の変換 (一般にただの型変換にとどまらないもの) は、専用の関数にラッ プしておくとよいでしょう。というのも、 C からテキストを受け取るときに は、いつも同じ処理をしなければならないからです。ラップを行うと、以下の ようになるでしょう:

cimport python_unicode
cimport stdlib

cdef unicode tounicode(char* s):
    return s.decode('UTF-8', 'strict')

cdef unicode tounicode_with_length(
        char* s, size_t length):
    return s[:length].decode('UTF-8', 'strict')

cdef unicode tounicode_with_length_and_free(
        char* s, size_t length):
    try:
        return s[:length].decode('UTF-8', 'strict')
    finally:
        stdlib.free(s)

おそらく、対象文字列の種類に応じて、より短い関数名をつけたいと思うこと でしょう。文字列の中身が違えば、受け取った時の処理方法も必然的に変わっ てくるからです。コードの可読性を高め、将来の変更に備えるために、文字列 のタイプごとに別の関数を用意しておくのがおすすめです。

テキストからバイト列へのエンコーディング

逆方向の、 Python unicode から C の char* への変換自体は、 メモリ管理済のバイト文字列 (memory managed byte string) を得るのでよけ れば、とても簡単です:

py_byte_string = py_unicode_string.encode('UTF-8')
cdef char* c_string = py_byte_string

上で触れたように、この操作は Python のバイト文字列オブジェクト内のバイ ト列バッファへのポインタを取得します。同じことを、 Python のバイト文字 列に対して、スコープ内の参照を維持せずに行うと、コンパイラエラーが発生 します:

# この書き方だとコンパイルできない !
cdef char* c_string = py_unicode_string.encode('UTF-8')

この書き方だと、Cython コンパイラは、コードが取得するのは一時的な文字 列へのポインタであって、代入直後にガベージコレクションされると判断しま す。無効になったポインタに後でアクセスすると、無効なメモリ領域を読み出 すことになり、おそらく segfault を起こすでしょう。そのため、 Cython は 上のような書き方のコンパイルを拒否するのです。

C++ の文字列

C++ ライブラリをラップする際、文字列はたいてい std::string クラスの形で受け渡されます。 C の文字列と同様、 C++ の文字列も、Python のバイト文字列との間で自動的に 型強制されます:

# distutils: language = c++

from libcpp.string cimport string

cdef string s = py_bytes_object
try:
    s.append('abc')
    py_bytes_object = s
finally:
    del s

ここでは、 C とはまた違ったメモリ管理が行われています。 C++ 文字列を生成すると、Python の文字列オブジェクトが保有しているのと は別の文字列バッファのコピーを生成します。そのため、一時的に生成した Python のオブジェクトから、直接 C++ の文字列への変換を行えます。この振 る舞いがよく使われるのは、 Python の unicode 文字列を C++ 文字列にエン コードするときです:

cdef string cpp_string = py_unicode_string.encode('UTF-8')

この処理では、まず unicode 文字列を一時的に生成した Python のバイト列 オブジェクトに変換してから、そのバッファの内容を新しく作成した C++ の 文字列にコピーしているので、ほんのちょっとオーバヘッドがあります。

反対向きについては、 Cython 0.17 以降で、より効率的なデコードのサポー トがあります:

cdef string s = string('abcdefg')

ustring1 = s.decode('UTF-8')
ustring2 = s[2:-2].decode('UTF-8')

C++ 文字列の場合、文字列のスライスをデコードすると、常に文字列の長さと して適切な値が考慮され、 Python におけるスライスのセマンティクス (例え ば、境界外のインデクスを指定すると空の文字列になる) が適用されます。

ソースコードのエンコーディング

コード中に文字列リテラルを記述する場合、ソースコードのエンコーディング は重要なポイントです。あるPython のバイト列リテラルについて、 Cython が C のコード中にどのようなバイト列を書き込むかは、ソースコードのエン コーディングによって決まります。 Cython は、 PEP 263 に沿った明示的 なソースファイルのエンコーディング指定をサポートしています。例えば、 パーザに ISO-8859-15 (Latin-9) でソースコードをデコードさせるには、 以下のようなコメントを ISO-8859-15 先頭に入れておきます:

# -*- coding: ISO-8859-15 -*-

エンコーディングの宣言が明示されていなければ、 PEP 3120 の定義に従っ て、ソースコードのエンコーディングを UTF-8 としてデコードを行います。 UTF-8 は非常に広く使われている文字コードで、 Unicode の文字セット全 体の表現が可能です。また、ASCII でエンコードされているテキストと互換性 があるため、効率的なエンコーディングが可能です。そのため、普段はほとん どASCII 文字で構成されているようなソースコードファイルを扱っているのな ら、UTF-8 を使うよう勧めます。

例えば、以下のような一行を UTF-8 でエンコードされたソースファイルに入 れた場合、 UTF-8 は文字 'ö' を 2 バイトシーケンスの '\xc3\xb6' にエンコードするため、実行すると 5 を出力します:

print( len(b'abcö') )

一方、以下のようなソースコードを ISO-8859-15 でエンコードしている と、文字 文字 'ö' は 1 バイトでエンコードされるので、実行すると 4 を出力します:

# -*- coding: ISO-8859-15 -*-
print( len(b'abcö') )

unicode リテラル u'abcö' は、どちらのケースでも、 4 つの Unicode 文字からなる文字列として正しくデコードされています。プレフィクスを付け ない Python のリテラル 'abcö' を使うと、 Python 2 ではバイト文字列 として扱われます (そのため、上のコードでは 4 文字または 5 文字になりま す)。 Python 3 では、プレフィクスのない文字列リテラルは Unicode 4 文字 の Unicode 文字列として扱われます。エンコーディングについて詳しくなけ れば、一見しただけではよくわからないかもしれません。詳しくは CEP 108 を参照してください。

経験則的には、非ASCII文字列の、プレフィクスのない str リテラルを使 うのは避け、テキストには全て unicode 文字列リテラルを使うのがベストで す。ちなみに、 Cython は __future__ から unicode_literals を import する機能をサポートしているので、 Python 3 と同様に、ソースコー ド中のプレフィクスのついていない str リテラルを全て unicode 文字列 リテラルとして解釈するよう指示できます。

単一バイトや単一キャラクタの扱い

Python の C-API は、通常の C の char 型を使ってバイト値を表 現しますが、Unicode のコードポイント値の表現、つまり 1 文字の Unicode キャラクタの表現用には、 Py_UNICODEPy_UCS4 の 二つの特殊な整数型があります。前者はバージョン 0.13 以降の Cython から サポートされ、後者はバージョン 0.15 から登場しました。 Py_UNICODE は、プラットフォームに応じて、符号なしの2バイトか 4バイトの整数、あるいは wchar_t で定義されています。 実際にどの型を使うかは、 CPython インタプリタをビルドするときのコンパ イル時オプションによって決まっていて、拡張モジュールでも、この設定を C でコンパイルするときに引き継ぎます。 Py_UCS4 の利点は、プラッ トフォームに関係なく、任意の Unicode コードポイント値を十分表現できる だけの大きな値を格納できるということにあります。 Py_UCS4 は 32 ビットの符号なし int または long として定義されています。

Cython では、Python オブジェクトから char に型強制したときの 挙動が、 Py_UNICODEPy_UCS4 から型強制したとき と異なります。 Python 3 のバイト列型での振る舞いと同様、 char はデフォルトの挙動として Python の整数値に型強制されま す。従って、以下のコードは A ではなく 65 を出力します:

# -*- coding: ASCII -*-

cdef char char_val = 'A'
assert char_val == 65   # ASCII encoded byte value of 'A'
print( char_val )

型強制の結果を Python のバイト文字列にしたいのなら、明示的に特定の型に 型強制するよう要求せねばなりません。以下のように書くと、今度は A (Python 3 では b'A') を出力します:

print( <bytes>char_val )

明示的な型強制は、 C の整数型全てに対して動作します。ただし、 charunsigned char の範囲を超える値を代入すると 実行時に OverflowError 例外を送出します。型強制は、型宣言した値に 代入するときにも自動的に行われます。例えば:

cdef bytes py_byte_string
py_byte_string = char_val

一方、 Py_UNICODEPy_UCS4 型は、 Python の unicode 文字列のコンテキストの外で使われることはほぼないので、 Python の unicode オブジェクトに型強制するのがデフォルトの動作になっています。 従って、以下のようなコードは A を出力します。 Py_UNICODE を使った場合も、同じようになります:

cdef Py_UCS4 uchar_val = u'A'
assert uchar_val == 65 # character point value of u'A'
print( uchar_val )

ここでも、明示的に型強制をすれば、この挙動をユーザ側でオーバライドでき ます。以下のコードは 65 を出力するはずです:

cdef Py_UCS4 uchar_val = u'A'
print( <long>uchar_val )

Unicode 文字の取りうるコードポイント値の最大値は 1114111 (0x10FFFF) なので、C の long (や、 unsigned long) へのキャ ストは問題なく行えます。 32ビット以上のプラットフォームでは、 int で十分です。

ナロー Unicode でビルドされている Python の場合

バージョン 3.3 以前の CPython で、ナロー Unicode ビルドのとき、つまり sys.maxunicode が 65535 のとき (Windows 版の全ビルドが該当します。 ワイド Unicode の場合には 1114111 です) でも、コードポイントの値が 16 ビット幅の Py_UNICODE 型に収まらないような Unicode 文字を扱 う方法があります。例えば、ナロー版の CPython は u'\U00012345' のよ うな unicode リテラルを扱えます。しかし、こういったケースでは、水面下 にあるシステムレベルのエンコーディングが Python 側に漏れてくるため、こ の文字列リテラルの長さは 1 ではなく 2 になってしまいます。こうした問題 は、文字列にわたるイテレーションや、インデクシングの際にも顕在化します。 さきの例では、 u'\uD808'u'\uDF45' といった文字が出てきます。 この二つの文字は、上の u'\U00012345' という文字を表すための、いわ ゆるサロゲートペア (surrogate pair) を形成しています。

この話題に関して詳しく知りたければ、 Wikipedia の UTF-16 エンコーディングに関する記事 (英文) を一読するとよいで しょう。

ナロービルドされた CPython 実行環境向けに Cython のコードをコンパイル するときにも、同じ特性があてはまります。大抵の場合、例えば、部分文字列 を検索するような場合には、テキストと文字列の両方にサロゲートが含まれて いるので、違いは無視できます。そのため、 Unicode を扱うコードはほとん どがナロービルドでもうまく動くでしょう。エンコードやデコード、出力も期 待通りに動作し、上のリテラルはナロー・ワイドの両方のプラットフォームで 同じバイト列になります。

しかし、プログラマは、一つの Py_UNICODE 値 (あるいは、 CPython における unicode 文字列中の「一文字」)が、ナロープラットフォー ム上では、Unicode 文字全てを表現するには小さすぎることを見過ごしてはな りません。 例えば、 u'\uD808'u'\uDF45' を unicode 文字列の中に見つけ たからといって、必ずしも文字列の中に u'\U00012345 が入っているとい うことにはならないのです。もしかしたら、文字列の中に異なる二つの文字が あり、たまたま件の文字のサロゲートペアと同じコード単位を持っているかも しれません。サロゲートペア中の二つのコードユニットは、互いに重複しない 値域をもっているので、コードポイントの列中からサロゲートペアを同定する ことができます。そのため、部分文字列としての検索なら、うまくいきます。

バージョン 0.15 以降、 Cython はサロゲートペアのサポートを拡張して、 ナロープラットフォームにおいても、 Py_UCS4 のレンジ内全ての 値に対して、 in を使った文字の検索を行えるようになりました:

cdef Py_UCS4 uchar = 0x12345
print( uchar in some_unicode_string )

また、ある大きなコードポイント値を持った文字列を、ナロー・ワイドどちら のプラットフォームでも Py_UCS4 の値に型強制できるようになりました。:

cdef Py_UCS4 uchar = u'\U00012345'
assert uchar == 0x12345

CPython 3.3 以降では、 Py_UNICODE 型はシステム固有の wchar_t 型へのエイリアスになっていて、 Unicode 文字列の内部 的表現とは切り離されました。その代わり、全ての Unicode 文字を、どのプ ラットフォームでも、サロゲートペアのことを考えずに表現できるようになり ました。そのため、バージョン 3.3 以降からは、 Py_UNICODE の サイズに関わらず、ナロービルドというものが存在しなくなりました。 詳しくは PEP 393 を参照 してください。

Cython 0.16 以降では、この変更を内部的に扱うようになりました。 また、型付けされていない変数との間で型の干渉がおきた場合か、プラット フォーム固有な Py_UNICODE の代わりに Py_UCS4 がソー スコード中で明に使われた場合に関しては、一文字の値に対しても正しい操作 を行います。 Cython が Python の unicode 型に対して最適化を行う際も、 C のコンパイル時に自動的に PEP 393 に従います。

イテレーション

Cython 0.13 から、 char*, バイト列、 unicode 文字列に対して、 ループ変数を適切に型づけすると、効率的にイテレーションできる機能がサポー トされました。以下のように書くと、望ましい C コードが生成されます:

cdef char* c_string = ...

cdef char c
for c in c_string[:100]:
    if c == 'A': ...

バイト列オブジェクトにも同様に書けます:

cdef bytes bytes_string = ...

cdef char c
for c in bytes_string:
    if c == 'A': ...

unicode オブジェクトの場合、 Cython はループ変数の型を自動的に推測して Py_UCS4 とみなします:

cdef unicode ustring = ...

# 注: 'uchar' の型づけは不要!
for uchar in ustring:
    if uchar == u'A': ...

型の自動推測は、大抵はより高速なコードにつながります。しかし、 unicode の操作の一部は依然として Python オブジェクトの値を必要とするため、結局 Cython がループの中でループ変数をむだに変換するかもしれません。特定の 一部のコードこのようなパフォーマンスの低下が起きるなら、ループ変数の型 を明に Python のオブジェクトにしたり、ループ中のどこかで変数の値を Python のデータ型の変数に代入したりして、オブジェクトを操作するまえに 型強制を一度だけ行なうとよいでしょう。

in テストの最適化もあり、以下のようなコードを C のコードで実行でき ます (実際には switch 文を使います):

cdef Py_UCS4 uchar_val = get_a_unicode_character()
if uchar_val in u'abcABCxY':
    ...

上のループ最適化と合わせると、 unicode パーザのような文字列変換コード をとても効率的にできます。