2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Python】日本語のモーラ列を仮名文字のシーケンスとして管理するクラス

Last updated at Posted at 2023-04-26

どれだけ需要があるのか分かりませんが、とりあえず形になったので公開します。

目次

はじめに
モーラを文字列リストで扱うときの問題点
MoraStr クラス
導入方法
モジュールの特徴
今後検討してみたいこと
C 言語で Python モジュールを作ることについて(ライブラリ開発者向け)
おわりに

はじめに

日本語の音の長さを数える単位としてモーラがあります。俳句や川柳、短歌でいう「字」はこのモーラに対応します。

Pythonで日本語の文章中に含まれるモーラを数えたり、モーラ単位で分割したりする手法については、既にいくつか記事があります。

[MeCabを使った575判定]

分割したモーラ列を管理する方法としては、各モーラに対応する文字列のリスト(あるいはタプル)を用いるのが手軽そうです。

['チョ', '', '', '', '']                   # 'チョコレート'
['', '', 'キュ', '', '', '', '', '']  # 'ガッキューシンブン'

たいていの場合はこれで事足りるのですが、少し不便なこともあります。

モーラを文字列リストで扱うときの問題点

切り出したモーラ列を再び文字列として扱う

リストのスライスを ''.join() で再結合する必要があります。記述量的には大したことないんですが、ちょっと見にくいです。

moras = ['', '', 'キュ', '', '', '', '', '']
text = ''.join(moras[:4])                                # 4モーラ目までを文字列化

部分モーラ列の判定

対象のモーラ列の中に任意の1モーラが含まれているかどうかは、<文字列> in <リスト> で判定できます。

moras = ['', '', 'キュ', '', '', '', '', '']
print('キュ' in moras)                                   # True

しかし、複数のモーラから成る「モーラ列」が含まれているかどうかを判定するには、この方法だとうまくいきません。左辺にリストを指定してもダメです。

moras = ['', '', 'キュ', '', '', '', '', '']
# True を返してほしい
print('キュー' in moras)                                 # False
print(['キュ', ''] in moras)                           # False

一番確実なやり方は、総当たりで調べることです1 2

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.lengthlen(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 拡張関連)

おわりに

ライブラリも記事も結構ニッチな感じが否めないのですが、刺さる人に刺さればいいなと思い、公開しました。気になったら使ってみてください!

外部リンク

  1. MoraStr クラスでは、モーラ列が長い場合に、より効率的なアルゴリズムを用いて探索を行います。

  2. モーラは特定の文字では始まらないことを前提に、正規表現により検索を行う方法も考えられます。

  3. 「マッチ」は「マッチャパフェ」の「部分文字列」であっても、「部分モーラ列」ではないと言えます。

  4. ただ、いずれにせよエラー時の処理は必要ですが。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?