どれだけ需要があるのか分かりませんが、とりあえず形になったので公開します。
目次
はじめに
モーラを文字列リストで扱うときの問題点
MoraStr クラス
導入方法
モジュールの特徴
今後検討してみたいこと
C 言語で Python モジュールを作ることについて(ライブラリ開発者向け)
おわりに
はじめに
日本語の音の長さを数える単位としてモーラがあります。俳句や川柳、短歌でいう「字」はこのモーラに対応します。
Pythonで日本語の文章中に含まれるモーラを数えたり、モーラ単位で分割したりする手法については、既にいくつか記事があります。
分割したモーラ列を管理する方法としては、各モーラに対応する文字列のリスト(あるいはタプル)を用いるのが手軽そうです。
['チョ', 'コ', 'レ', 'ー', 'ト'] # 'チョコレート'
['ガ', 'ッ', 'キュ', 'ー', 'シ', 'ン', 'ブ', 'ン'] # 'ガッキューシンブン'
たいていの場合はこれで事足りるのですが、少し不便なこともあります。
モーラを文字列リストで扱うときの問題点
切り出したモーラ列を再び文字列として扱う
リストのスライスを ''.join()
で再結合する必要があります。記述量的には大したことないんですが、ちょっと見にくいです。
moras = ['ガ', 'ッ', 'キュ', 'ー', 'シ', 'ン', 'ブ', 'ン']
text = ''.join(moras[:4]) # 4モーラ目までを文字列化
部分モーラ列の判定
対象のモーラ列の中に任意の1モーラが含まれているかどうかは、<文字列> in <リスト>
で判定できます。
moras = ['ガ', 'ッ', 'キュ', 'ー', 'シ', 'ン', 'ブ', 'ン']
print('キュ' in moras) # True
しかし、複数のモーラから成る「モーラ列」が含まれているかどうかを判定するには、この方法だとうまくいきません。左辺にリストを指定してもダメです。
moras = ['ガ', 'ッ', 'キュ', 'ー', 'シ', 'ン', 'ブ', 'ン']
# True を返してほしい
print('キュー' in moras) # False
print(['キュ', 'ー'] in moras) # False
def has_moras(moras, submoras):
mora_cnt = len(moras)
submora_cnt = len(submoras)
for i in range(mora_cnt - submora_cnt + 1):
matched = True
for j in range(submora_cnt):
if moras[i+j] != submoras[j]:
matched = False
break
if matched:
return True
return False
moras = ['ガ', 'ッ', 'キュ', 'ー', 'シ', 'ン', 'ブ', 'ン']
submoras = ['キュ', 'ー']
print(has_moras(moras, submoras)) # True
少々ややこしくなりましたね。
ちなみに、通常の文字列にしてから in
で確かめる方法だと、次のような場合にうまくいきません3。
moras = ['マ', 'ッ', 'チャ', 'パ', 'フェ']
submoras = ['マ', 'ッ', 'チ']
# False を返してほしい
print(''.join(submoras) in ''.join(moras)) # True
それは本当にモーラに対応する文字列か?
これまでの例では、文字列がすでにモーラ単位で適切に分割されていることを前提としてきましたが、実行時、文字列リスト内にモーラとは対応しない余計な文字が含まれていないかどうかは、書き手が保証する必要があります。そうでないと、気付かないうちに間違った結果を得てしまうかもしれません。
suffix = list('GPT')
moras = ['チャ', 'ッ', 'ト']
new_moras = moras + suffix
# 日本語としてのモーラ数がほしいが…
print(len(new_moras)) # 6
上のような単純な例だとすぐに気付くかと思いますが、関数やモジュールをまたいで文字列リストのやり取りをする場合には注意が必要です。
MoraStr クラス
ここで、私が作った MoraStr
クラスの紹介です。
MoraStr
クラスは、上に挙げたような面倒な処理やチェックを内部でやってくれます。そのため、モーラ列を扱う記述を簡素化できます。
MoraStr クラスの基本的な使い方
インストールに関してはこちら。
仮名文字列(カタカナかひらがな)をコンストラクタに渡して使います。以下はREPLでの実行例です。
# MoraStrクラスをインポート
>>> from morastrja import MoraStr
# インスタンスの作成
>>> MoraStr('モーラ')
MoraStr('モ' 'ー' 'ラ')
>>> MoraStr('シミュレーション')
MoraStr('シ' 'ミュ' 'レ' 'ー' 'ショ' 'ン')
リストやタプルと同じように添え字アクセスできます。
# 添え字アクセス
>>> MoraStr('アーティキュレーション')[3] # 0-indexed で3モーラ目
'キュ'
>>> MoraStr('アーティキュレーション')[-2] # 後ろから2モーラ目
'ショ'
# スライス
>>> MoraStr('ジェットエンジン')[:3] # 最初の3モーラを抽出
MoraStr('ジェ' 'ッ' 'ト')
MoraStr
オブジェクトには 2つの属性があります。.length
属性でモーラ数が取得でき、.string
属性で分割されていない文字列表現が取得できます。moras.length
と len(moras)
は同じ値を返します。好きな方を使ってください。
# .length 属性
>>> MoraStr('クッション').length # モーラ数を取得
4
>>> len(MoraStr('クッション')) # 上と同じ
4
# .string 属性
>>> moras = MoraStr('クッション')
>>> moras
MoraStr('ク' 'ッ' 'ショ' 'ン')
>>> moras.string # 分割前の文字列表現を取得
'クッション'
部分モーラ列に対応する文字列の取り出し
MoraStr
クラスを使えばこう書けます
moras = MoraStr('ガッキューシンブン')
# MoraStr('ガ' 'ッ' 'キュ' 'ー' 'シ' 'ン' 'ブ' 'ン')
text = moras[:4].string # 4モーラ目までの文字列
# 'ガッキュー'
.string
属性で文字列を取り出しています。''.join()
する必要はありません。
部分モーラ列の判定
moras = MoraStr('ガッキューシンブン')
# MoraStr('ガ' 'ッ' 'キュ' 'ー' 'シ' 'ン' 'ブ' 'ン')
print('キュ' in moras)
# True
print('ギュ' in moras)
# False
print('キュー' in moras)
# True
print(MoraStr('キュー') in moras)
# True
print('ギュー' in moras)
# False
部分モーラ列が 1モーラであってもそうでなくても、期待した通りの結果が得られます。
以下のどちらの書き方でも OK です。
<文字列> in <MoraStr オブジェクト>
<MoraStr オブジェクト> in <MoraStr オブジェクト>
moras = MoraStr('マッチャパフェ')
# MoraStr('マ' 'ッ' 'チャ' 'パ' 'フェ')
submoras = MoraStr('マッチ')
# MoraStr('マ' 'ッ' 'チ')
print('マッチ' in moras)
# False
print(submoras in moras)
# False
上の例のような場合にも、「マッチャパフェ」の部分モーラ列ではない「マッチ」に対して、True
が返されることはありません。
バリデーション
MoraStr
クラスのコンストラクタにひらがなや半角カタカナを渡すと、自動的に全角カタカナに変換されます。
moras = MoraStr('ひらがな')
# MoraStr('ヒ' 'ラ' 'ガ' 'ナ')
moras = MoraStr('ハンカクモジ')
# MoraStr('ハ' 'ン' 'カ' 'ク' 'モ' 'ジ')
それ以外の文字列を含んでいるとエラーとなり、インスタンスは作成されません。
moras = MoraStr('漢字')
# ValueError
moras = MoraStr('latin alphabet')
# ValueError
また、MoraStr
オブジェクトはイミュータブルであり、インスタンス作成後の値の変更は不可です。
moras = MoraStr('カタカナ')
moras[3] = 'タ'
# TypeError
ですので、以下のコードで new_moras
内部の文字列表現にカタカナ以外の文字列が混入してしまうことはありません。
moras = MoraStr('チャ' 'ッ' 'ト')
new_moras = moras + suffix # 文字列としてカタカナのみを含むことが保証される
これにより、気付かずにモーラを数え間違ってしまうことを未然に防げます4。
導入方法
MoraStr
クラスは、morastrja モジュールから利用できます。pip を通じてインストール可能です。
pip install morastrja
ビルドにはCコンパイラ(C99以降)が必要です。
モジュールの特徴
-
pip install
だけで導入でき、他のサードパーティーライブラリを必要としません。 -
MIT ライセンスです。
-
型ヒントのスタブを同梱しています。
-
コア部分は C で実装されているため、同じ内容の処理を Python で書くよりも高速です。
-
MoraStr
オブジェクトは、イミュータブルでハッシュ可能です。辞書や集合のキーとして安全に使えます。 -
MoraStr.find()
MoraStr.count()
など、文字列型と共通のメソッドを多く備えています。 -
モーラ文字列の前処理に便利なユーティリティー関数が利用できます:
-
utils.vowel_to_choon()
カタカナ文字列中の同一母音の連続を長音記号(ー)に変換します。 -
utils.choon_to_vowel()
カタカナ文字列中の長音記号(ー)を対応する母音に変換します。
-
詳しくは、ドキュメントをご覧ください。
今後検討してみたいこと(やるとは言ってない)
-
C/C++ から直接呼び出せる API の提供
-
SIMD の使用
-
utils の追加
-
このライブラリを活用した、より高次なライブラリの作成
C 言語で Python モジュールを作ることについて(ライブラリ開発者向け)
この項目は、主にこれから Python の C/C++ 拡張を作ろうと思っている人が対象です。
私自身、C 言語は初心者同然ですが、勉強もかねて Python C/API を使ってライブラリのコア部分を実装してみました。正直、Python クラスを C 言語で書くのはあまりおススメできません。細かい罠が多く、ネット上に公開されているコード片が正しく動かないこともままあります。あと、単純に実装量が多くなります。 他でも書かれていますが、やっぱ拡張モジュールの作成は Cython や pybind11 を使うのが楽なんじゃないかと思います。私は詳しくないですが、Rust 向けの Python バインディング (PyO3) もあるので、特に Rust に慣れている人はそちらを試してみるのもいいと思います。
拡張モジュールを作るときの注意点としては、やはり参照カウントの管理でしょうか。API を使うときに、それが new reference を返すのか borrowed reference を返すのか、しっかり確認する必要があります。私が書いたものは、色々とお行儀の悪いコードになっていますが、そこに関しては気を付けて実装したつもりです。C++ を使う場合、std::vector に Python オブジェクト(ポインタ)を格納するのはメモリ関係のバグの元になりやすいですし、避けた方がいいです。STL は CPython にないデータ構造を扱いたい場合には便利ですが、動的配列なら Cpython にもありますし、std::vector 自体は別に速いわけでもないので、普通に PyListObject を使いましょう。循環参照も GC が解放してくれます。
参考にしたページ(CPython 拡張関連)
-
Python/C API リファレンスマニュアル
公式です。質・量ともに充実しています。CPython 拡張を作るなら、まずざっと目を通しておくといいと思います。 -
年末が近づくと Python/C API を無駄に使いたくなるので準備
C 拡張が動くようにするために必要な手続きや注意すべき点についてまとめられています。 -
【Python/C API】PythonとC言語の関数とで様々なデータをやりとりする(リスト・辞書・キーワード引数など)
参照カウントの管理について、例と共に詳しく書かれていて良いです。ただ、例外処理についてももう少し丁寧にやってくれていいかなと感じました。
おわりに
ライブラリも記事も結構ニッチな感じが否めないのですが、刺さる人に刺さればいいなと思い、公開しました。気になったら使ってみてください!