モジュール docutils.statemachine

個人的に Docutils でもっとも興味のある機能が本節で見ていくクラス StateMachine およびそれに付随する一連の関係機能だ。 Docutils は reStructuredText というテキスト形式を処理するために存在するパッケージであり、この状態機械はまさにその処理の主役級の機能なのだ。実装を調べていくと、このモジュールの構造は上層と下層の二段構えになっている。下層は reStructuredText のテキスト解析に特化したマシーンで、上層がより抽象的なテキスト解析の手続きの枠組みを提供する体になっている。

私の目的はこの上層部を reStructuredText ではなく、もっと別のテキスト形式、例えば趣味の麻雀ゲームの牌譜や、ダンジョンマスター (RTC) のスクリプトの解析に応用できないかと考えている。

本節では最初に関連するクラス図を示す。それから抽象クラス、上層部の機能をおおまかに理解し易さ重視で見ていく。次にいくつかのテキスト解析例を示し、最後に本モジュールに関する感想を述べる。

クラス図

クラス StateMachine および State から派生したクラス群を大雑把に表現した図を示す。他のページの図と同様に、正確さよりも理解し易さを重視して若干の省略とウソが入っている。

classDiagram direction TB Node <|-- document StateMachine *-- State: states StateMachine <|-- StateMachineWS StateMachineWS <|-- RSTStateMachine State <|-- StateWS StateWS <|-- RSTState RSTState <|-- ConcreateRSTState StateMachineWS <|-- NestedStateMachine RSTState *-- NestedStateMachine StateMachine <|-- SearchStateMachine _SearchOverride <|-- SearchStateMachine _SearchOverride <|-- SearchStateMachineWS StateMachineWS <|-- SearchStateMachineWS class StateMachine{ str input_lines int input_offset str line int line_offset str initial_state str current_state +unlink() +run(...) +get_state(str) +attach_observer(...) +detach_observer(...) -notify_observers() } class State{ #dict patterns$ #list initial_transitions$ #dict nested_sm_kwargs$ +runtime_init() +unlink() #add_initial_transitions() #add_transitions(...) +remove_transition(str) +no_match(...) +bof(...) +eof(...) #nop(...) } State --> "1" StateMachine: state_machine class StateMachineWS{ +get_indented(...) +get_known_indented(...) +get_first_known_indented(...) } class StateWS{ #dict indent_sm_kwargs$ #dict known_indent_sm_kwargs$ #ws_patterns$ #ws_initial_transitions$ #add_initial_transitions() #blank(...) #indent(...) %%+known_indented(...) %%+first_known_indented(...) } class _SearchOverride{ #match(str) } StateWS --> StateMachine: indent_sm StateWS --> StateMachine: known_indent_sm class RSTStateMachine{ +run(...) } class NestedStateMachine{ +run(...) } RSTStateMachine --> document: document RSTStateMachine --> Node: node NestedStateMachine --> document: document NestedStateMachine --> Node: node class RSTState{ #nested_sm +runtime_init() #no_match(...) #bof(...) +nested_parse(...) +nested_list_parse(...) }
  • 図内に示されているクラスは次のどちらかにある。

    • モジュール statemachine

    • モジュール parsers.rst.states

  • GoF デザインパターンでいうところの State パターン、それに Observer パターンが採用されている。本節で見たいのは前者の詳細。

コード読解

モジュール statemachine の docstring にかなり詳細な説明文が記されている。興味のある部分だけかいつまんで記す。

上層クラス概要

  • クラス StateMachineState がそれぞれ状態機械と状態を抽象的に表現する。

  • クラス StateMachineWSStateWS のように末尾が 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_smState.nested_sm_kwargs を適宜上書きする。前者には StateMachine またはそのサブクラスの型を、後者には そのコンストラクターに渡す引数を dict オブジェクトをそれぞれ指定する。

    例:クラス RSTState の実装

  • WS 系で実装する場合は、さらに次のものの上書きも適宜行う。

    • indent_sm

    • indent_sm_kwargs

    • known_indent_sm

    • known_indent_sm_kwargs

    • indent()

    • 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 をオーバーライドするサンプルを書きたい。どうも処理テキストにあるインデントが意味を持つような場合に有用らしい。