モジュール docutils.statemachine¶
個人的に Docutils でもっとも興味のある機能が本節で見ていくクラス
StateMachine およびそれに付随する一連の関係機能だ。 Docutils は
reStructuredText というテキスト形式を処理するために存在するパッケージであり、この状態機械はまさにその処理の主役級の機能なのだ。実装を調べていくと、このモジュールの構造は上層と下層の二段構えになっている。下層は reStructuredText のテキスト解析に特化したマシーンで、上層がより抽象的なテキスト解析の手続きの枠組みを提供する体になっている。
私の目的はこの上層部を reStructuredText ではなく、もっと別のテキスト形式、例えば趣味の麻雀ゲームの牌譜や、ダンジョンマスター (RTC) のスクリプトの解析に応用できないかと考えている。
本節では最初に関連するクラス図を示す。それから抽象クラス、上層部の機能をおおまかに理解し易さ重視で見ていく。次にいくつかのテキスト解析例を示し、最後に本モジュールに関する感想を述べる。
クラス図¶
クラス StateMachine および State から派生したクラス群を大雑把に表現した図を示す。他のページの図と同様に、正確さよりも理解し易さを重視して若干の省略とウソが入っている。
図内に示されているクラスは次のどちらかにある。
モジュール
statemachineモジュール
parsers.rst.states
GoF デザインパターンでいうところの State パターン、それに Observer パターンが採用されている。本節で見たいのは前者の詳細。
コード読解¶
モジュール statemachine の docstring にかなり詳細な説明文が記されている。興味のある部分だけかいつまんで記す。
上層クラス概要¶
クラス
StateMachineとStateがそれぞれ状態機械と状態を抽象的に表現する。クラス
StateMachineWSとStateWSのように末尾がWSのものは、処理テキストの空白文字を「気にする」版。クラス
SearchStateMachineのように、先頭がSearchから始まるものは、テキストの正規表現による検索をre.searchにより実現する版。通常版はre.matchによる。
処理対象のテキストの特性に応じて、空白と検索のスタイルをクラスの選択で決めろということだろう。
モジュール statemachine の利用法¶
クライアントコードの骨格はこうなる。まずは WS でも Search でもない版を説明する。
from statemachine import StateMachine, State, string2lines
import re
# Derive subclasses of State for your state machine...
class InitialState(State):
patterns = {'atransition': r'pattern', ...}
initial_transitions = ['atransition', ...]
def atransition(self, match, context, next_state):
# do something
result = [...]
return context, next_state, result
# More methods for transitions...
# More subclasses...
sm = StateMachine(state_classes=[InitialState, ...],
initial_state='InitialState')
with open('inputfile') as input:
input_string = input.read()
input_lines = string2lines(input_string)
results = sm.run(input_lines)
sm.unlink()
各種スーパークラスおよび補助関数を
importする。また、状態クラスを定義するのであれば間違いなく Python 標準のモジュールreも要る。状態遷移図に基づいて、各状態に対応する
Stateのサブクラスを定義する。クラスデータ
patternsはメソッド名と正規表現の辞書だ。ここのある正規表現と状態機械が今見ている文字列とマッチしたときに、キーとなるメソッドが呼び出される仕組みだ。クラスデータ
initial_transitionsはメソッド名のリストである。ここは正確に理解していないが、実行時に機械から呼び出されるメソッドはここにも格納されている必要がある。この「正規表現と関連付けられたメソッド」の実装が本質的にはテキスト処理をする。
match: 正規表現オブジェクトのmatch呼び出し結果オブジェクトcontext: アプリケーション依存のデータnext_state: 状態遷移図におけるこの状態の次の状態の名前(文字列)ある状態から次の状態へ遷移することを指示するには、メソッドの戻り値の
next_stateに状態クラス名を文字列で指定する。例えば次の状態をクラスSecondStateにしたいのならば次のようにする。こうすることで、次に呼びだされるメソッドがクラスSecondStateのリストinitial_transitionsのどれかが示すメソッドになるはず。def atransition(self, match, context, next_state): # do something result = [...] if some_condition: next_state = 'SecondState' return context, next_state, result
StateMachineオブジェクトを直接生成する。引数
state_classesには状態機械の取り得る状態遷移を列挙して指定する。動作効率を配慮すれば、先に遷移先になりそうな状態名というかクラスを、リストのより先頭に指定するのが良いように思える。通常は動作さえすれば良いので、好きな順にクラスを書いて良い。引数
initial_stateには状態遷移の初期状態に相当するクラス名を文字列として指定する。
補助関数
string2linesはこれはオマケのようなもの。StateMachine系は元の(巨大な)文字列を行ごとに分割したものを処理することになる。そこで、ファイルの内容を改行文字でsplitしてリスト化したオブジェクトが要る。このためにこの補助関数を使うに過ぎない。メソッド
runを呼び出すことで状態遷移が始まる。この説明コードでは結果がresultsに出力されるように読めるが、引数contextを明示して、状態オブジェクトに操作させてそれを出力とする方法がある。最後の
unlink呼び出しは一種のクリナップメソッド。書き忘れたとしても何ということはない。
クラス State¶
前項で述べたこと以外を記す。
メンバーデータ
self.state_machineで状態機械オブジェクトにアクセスする。どの
self.transitionsの遷移メソッドの正規表現パターンにもマッチしないテキストが現れた時に、メンバーメソッドno_matchが呼び出される。エラー処理に使えるだろう。メソッド
bofおよびeofは入力「ファイル」の先頭および末尾の到達時に呼び出される。これもオーバーライドしない限りは何もしない。メソッド
nopは何もしない遷移メソッド。単に状態を遷移させるためだけに有用。
入れ子状態機械¶
話がややこしくなるが、状態機械の中に状態機械を入れることができる。
State.nested_smとState.nested_sm_kwargsを適宜上書きする。前者にはStateMachineまたはそのサブクラスの型を、後者には そのコンストラクターに渡す引数をdictオブジェクトをそれぞれ指定する。例:クラス
RSTStateの実装WS系で実装する場合は、さらに次のものの上書きも適宜行う。indent_smindent_sm_kwargsknown_indent_smknown_indent_sm_kwargsindent()known_indent()first_known_indent()
例:不明
応用¶
クラス
RSTStateMachineおよびRSTStateさらにその膨大なサブクラス群。某麻雀ゲームの牌譜を処理するスクリプトを
docutils.statemachineを再利用することで実装できた。別リポジトリーのファイルだが、アドレスを次に示す。<https://github.com/showa-yojyo/bin/blob/master/mjstat/states.py>
牌譜となるテキストデータのサンプルもこのパスの近所にあるので、スクリプトとテキストを交互に観察して、サブクラスの作り方の要領をつかめると思う。ただし、私の状態クラスの設計があまり良くない。おそらく WS 系を用いれば、テキストがインデント指向になっていることを上手く利用できるハズ。
あとは Dungeon Master (RTC) のスクリプトの解析スクリプトを作りたいという考えがある。こちらはスクリプトというか、どちらかというと ini ファイルのような風味があるテキスト。
感想¶
モジュール
statemachine上層部の機能は reStructuredText 無関係にエンドユーザー(私)が利用できる。型を取り扱うのに二つのマナーがあることに注意。すなわちクラスを直接指示するものと、クラス名を文字列で指示するものだ。コードの位置がクライアント方面に近づくほど、文字列での指定が多くなりがちなのか。
入れ子処理を試したい。つまり
State.nested_smをオーバーライドするサンプルを書きたい。どうも処理テキストにあるインデントが意味を持つような場合に有用らしい。