Pythonの状態遷移パッケージ(transitions)を理解する【準備編】

# この記事の対象と目的
__① 対象__
組込み分野等において、ソフトウェアの動的設計や実装方法として使われる「状態遷移」をPythonで実現するための手段を紹介します。RaspberryPi等の組込みLinuxを用いて簡単な制御機器を作る際、Pythonを使って状態遷移を実現する場合の参考にしてもらえればと思います。

__② 当記事シリーズの目的__
Pythonの状態遷移を実現できる超マイナー（と言ったら失礼）なパッケージ「transitions」の使い方・注意点等について紹介する記事です。
機能の豊富さの割にはあまり日本語で解説しているサイトや記事を見ないので、何回かに分けて詳細について説明したいと思います

## 「transitions」について
組込みシステム/ソフトウェア開発でよく使用される状態遷移ですが、その実装についてはCを主として様々な言語で実装されます。「transitions」は前述のとおりPythonで状態遷移を実現するパッケージとなります。
https://github.com/pytransitions/transitions

Pythonでもいくつかの状態遷移を実現する方法やパッケージがありますが、あえて「transitiosn」を選ぶ理由としては以下が挙げられます。

 * 豊富な機能。特に定義したステートマシンの表示機能はデバッグや理解に有用
 * 少しずつではあるがパッケージが更新されている
 * 公式の豊富なチュートリアル

## チュートリアル
簡単なチュートリアルについては以前の記事を見てもらえればと思います。
[Pythonで状態遷移(transitions)](https://qiita.com/noca/items/f7ffd4acc641a809ac67)

## 今回の話
今回は準備編と題して基本的なインストールから、チュートリアルでは触れなかったOSごとの画像出力機能のインストールと設定方法について説明します。
状態遷移の具体的な記載方法については後の記事にゆずるので、インストールと設定が済んでいる方はこの記事は飛ばしてもらって構いません。

# transitionsパッケージのインストール方法
基本的なインストールは毎度おなじみpipで行います。これはPython環境の入ったLinux、MacOS、Windowsに共通の操作になります。

```
pip install transitions
```

これにより、チュートリアルのようにtransitionsを使用することができるようになります。

### 動くか確認してみる
細かな動作については別の記事で紹介しようと思いますので以下のコードは読み飛ばしてもらっても構いませんが、動作確認してみたい方は以下のコードをJupyter Notebook等に張り付けて動かしてもらえればと思います。

#### ①動作する最小構成のステートマシンの定義
まずは状態遷移を管理するステートマシンの定義を行います。
以下のコードによりステートマシンを定義でき、実行することによりmodelオブジェクトにステートマシンが実装され初期状態(state)は「'A'」になります。

```python
#動作する最小構成のステートマシンの定義
from transitions import Machine
states=['A', 'B', 'C']    #状態の定義

class Matter(object):
    pass

model = Matter()
machine = Machine(model=model, states=states, initial='A', 
                  auto_transitions=False, ordered_transitions=True)
```
#### ②動作確認
上記で定義したステートマシンを実際に動かしてみます。
詳細は割愛しますが状態の確認はstateメソッドで出来ます。今回は遷移と遷移のイベントとなるtriggerを定義していないので、自動で設定されたnext_stateメソッドで次の状態になることが確認できます。
※next_stateなんかも後の記事で紹介するので、今はそういうモノだと思ってもらえればと思います。

```python
>>> model.state            # 初期状態の確認
'A'
>>> model.next_state()     # 次の状態へ遷移
True
>>> model.state            # 現在の状態の確認
'B'
>>> model.next_state()     # 次の状態へ遷移
True
>>> model.state            # 現在の状態の確認
'C'
```

ちょっとわかりにくいですが状態変化を起こさせるメソッドを実行する対象は、通常machineオブジェクトではなくmodelオブジェクトになるので注意。
一通り定義したステートマシンの状態遷移の動作は確認できたと思います。


# ステートマシンの表示について
上記まででおしまい！だとチュートリアルと何ら変わらないので、ここではtransitionsを扱う上で助けとなるステートマシンの表示機能とそのインストール方法を紹介します。
[チュートリアル](https://qiita.com/noca/items/f7ffd4acc641a809ac67)を実施した方はわかると思いますが、チュートリアルのコードをそのまま実施してもチュートリアルのような状態遷移図は生成されません。
実際に定義したステートマシンの表示や画像ファイルへの出力についてはtransitionsのGraphMachineを使うとできますが、transitions以外のパッケージが必要な上、一部OSごとにインストールが異なるのと注意点があるのでそれについても説明します。

### GraphMachineとは
GraphMachineはMachineクラスを拡張したものになります。
Machineクラスと同様にクラスに指定したstates, transitions, コールバックなどをmodelに付与しステートマシンを実装することが出来ますが、それだけではなく定義したステートマシンや現在の状態や遷移を画像として出力することができます。もちろん出力された画像はファイルとして保存したり、Jupyter Notebook上などで表示することが可能です。

## ステートマシンの表示に必要なパッケージ
GraphMachineで作成される画像オブジェクトは外部パッケージに依存しているため、それら関連パッケージをインストールする必要があります。

### Jupyter Notebook使用時に必要な共通パッケージ
Jupyter Notebook等を入れている場合はGraphMachineで作ったステートマシンの状態を直接表示することができます。その場合は（Windows/Linux/macOS関係無く）以下のパッケージが必要になります。Jupyter Notebookが入っていれば基本的にインストールされると思いますが念のため。

```
pip install IPython
```

### GraphvizとPyGraphvizのインストール
次に本題となるステートマシンを画像出力するために必要な外部パッケージGraphvizとPygraphvizのインストールについて説明します。
transitionsの画像出力についてはこれらパッケージに依存しているため、これがないとどうにもなりませんが、ちょっとしたコツ？のようなものと注意点があります。
※ちなみに次期Versionの0.70ではPyGraphvizは使用しない方向になっているので、Version0.69までの対応となる見込みです。（「Windowsの場合」参照のこと）

MachineクラスそのものはOSに依存しませんが、GraphMachineが描画オブジェクトを作成するために使用しているパッケージがOSに依存するため、それぞれOSに合わせてインストールする必要があります。

#### Linuxの場合
まずはgraphvizとgraphviz-devをLinux上にインストールします。
私の環境（Ubuntu 16.04/64bit）では以下の通り。Raspberry Pi用OSのRaspianでも同様なはず。

```
sudo apt-get install graphviz graphviz-dev
```

次にpipによりPyGraphvizをインストールします。

```
pip install pygraphviz
```

注意：もしもpygraphvizインストール時にエラーが発生しインストールできなかった場合は以下のコマンドを試してみてください。

```
pip install pygraphviz --global-option=build_ext --global-option=-I/usr/include/graphviz --global-option=-L/usr/lib/graphviz --upgrade --force-reinstall
```

上記において、-I/usr/include/graphvizは「graphviz-dev」をインストールした時に配置されたGraphvizのincludeフォルダで、-L/usr/lib/graphviz/はGraphvizのlibraryフォルダとなります。


#### macOSの場合
macではhomebrewが入っていることが前提。まずはgraphvizのインストール。

```
brew install graphviz
```

次にpygraphvizパッケージのインストール。こちらはLinuxと若干異なる可能性があるので注意。
私の環境（macOS 12.0.1）では以下のコマンドで導入することができました。

```
pip install pygraphviz --install-option=--include-path=/usr/local/include/graphviz/ --install-option=--library-path=/usr/local/lib/graphviz/
```

上記において、-I/usr/local/include/graphvizは「graphviz」をインストールした時に配置されたGraphvizのincludeフォルダで、-L/usr/local/lib/graphviz/はGraphvizのlibraryフォルダとなります。

#### Windowsの場合
Windowsの場合はちょっと他とやり方が違うので注意。（transitions 0.69時点）
今のところ以下①もしくは②の方法でインストールが可能です。ただし②の方法は致命的な問題を抱えているので①の方法を推奨します。

__方法①__
初めにGraphvizをインストールします。なお(Ana)condaが入っている場合はconda経由で入れる必要があるので注意。

 * (Ana)condaが入っていない場合
    Graphvizを[Graphviz公式](https://graphviz.gitlab.io/_pages/Download/Download_windows.html)よりダウンロードし、インストールする。
    dot.exeなどがあるGraphvizのbinのパスを通す（環境変数PATHに設定する）

 * (Ana)condaが入っている場合
    conda経由でGraphvizとpython-graphvizをインストール。

    ```
    conda install graphviz
    conda install python-graphviz 
    ```

次に以下コマンドによってtransitionsのdev-graphvizブランチをインストールします。すでにtransitionsパッケージをインストール済みの場合はアンインストールしておきます。
なお以下のコマンドを使うにはgitが必要になりますので[GitHub](https://gitforwindows.org/)などからwindows版をダウンロードし、インストールしておきます。

```
pip install git+https://github.com/pytransitions/transitions.git@dev-graphviz
```

補足：当方法ではLinux/macOS版と異なり、GraphMachine内部ではPyGraphvizではなくGraphvizそのものが用いられます。したがって①の方法ではPyGraphvizをインストールする必要はありません。
参考：[Try the sample with GraphMachine extension #324 - comment](https://github.com/pytransitions/transitions/issues/324#issuecomment-438878104)

__方法②__
以下のやり方で有志の作ったGraphviz/64bit版と改造PyGraphvizを入れます。(後述の問題より激しく非推奨)
　[python3の環境上でpygraphvizを入れる(windows7 64bit)](https://hytmachineworks.hatenablog.com/entry/2017/01/25/215655)

ちなみに②の方法は64bit版Graphvizに対応するために直接PyGraphvizのコード修正を伴ったり、パッケージのインストール方法が異なったりと非常に面倒なので①の方法で都合の悪い場合以外は選択しないで良いです（一応忘備録として記載）。

注意：方法②のWindowsにおけるPyGraphvizを用いたGraphMachineの描画ですが、現在「自己遷移をすると例外が発生して描画できない」というバグを抱えています（例えばtransitionsの定義で、sourceとdestが同じ場合など）。
これは以下の通り公式でもアナウンスされていますが、PyGraphvizとWindowsとの問題らしくtransitions側の問題ではないとのこと。実際に以下のissueはクローズされていますが問題そのものがなくなっているわけではありません。
参考：[transitions/KeyError: 'agedge: no key'](https://github.com/pytransitions/transitions/issues/258)

2018/12/2現在のtransitionsの最新Versionは0.69で、上記「Windosw上のPyGraphvizによる問題」を回避するために次期updateのVersion0.70でPyGraphvizを使わないでGraphMachineを実現する方式が進められています。現在の回避策としては上記①の方法になりますが、正式版のtransitions 0.70を待つのも良いかもしれません（グラフ表示しないMachineクラス等は0.69でもWindowsでもLinux/macOSでも相違なく使えます）。
参考：[transitions/Replace pygraphviz with graphviz #325](https://github.com/pytransitions/transitions/issues/325)

### GraphMachineを用いて状態遷移を描画してみる。
面倒なことに現状はPyGraphvizの問題により、PyGraphvizを用いる版とPyGraphvizを用いない版ができてしまいましたので、それぞれについて説明します。とはいえGraphMachineそのものの設定について異なるわけではなく、異なるのは実際にGraphMachineから出力されたオブジェクトを表示する部分のみになります。

#### GraphMachineの定義について
こちらはLinux/macOS/Windowsで共通の内容。
状態遷移をグラフ表示させるにはGraphMachineを用いて生成する必要がありますが、これは今までMachineで定義していたものをGraphMachineに置き換えて、いくつかの引数を設定してやるだけで良いです。
ひとまず、冒頭の例をGraphMachineで定義しなおしてみましょう。

```Python
from transitions.extensions import GraphMachine
states = ['A', 'B', 'C']    #状態の定義

class Matter(object):
    pass

model = Matter()
machine = GraphMachine(model=model, states=states, initial='A', 
                       auto_transitions=False, ordered_transitions=True,
                       title="", show_auto_transitions=False, show_conditions=False)
```

Machine時の定義と変わったところといえば、import部分とmachineインスタンスを生成する部分になります。
GraphMachineはtrantisionsの拡張機能になりますのでextensionsにあることに注意。
またGraphMachineそのものの定義として、引数の前半はMachineと全く同じですが、title, show_auto_transitions, show_conditions引数はGraphMachine用に拡張されたものになります。
これら引数以外は基本的にステートマシンそのものの扱いはMachineクラスと同じで、GraphMachineで定義されたステートマシンもMachineで定義されたものと同様な振る舞いをします。

ちなみに上記コードを実施しただけではグラフ表示できません。グラフ表示するにはGraphMachineでステートマシン機能を付与したインスタンス(ここではmodelオブジェクト)を用いて画像オブジェクトを生成し、それをファイルに保存したりIPython上に表示したりします。

補足：きちんとGraphvizやPyGraphvizがインストールされていないとGraphMachineの呼び出し時に例外が発生するので注意。

#### GraphMachineからの画像オブジェクトの生成について
GraphMachineから画像オブジェクトの生成方法と、ファイル保存の仕方や表示について説明しますが、ここからOS間の違い（というかPyGraphvizとGraphvizでインストールした場合の違い）が生じるので、OS毎に説明します。
ここでは既に上記コードが実行され、GraphMachineによってmodelオブジェクトにステートマシンが割り当てられたことを前提とします。

#### Linux/macOSの場合 (PyGraphvizをインストールしている場合)
Linux/macOSでは「GraphvizとPyGraphvizのインストール」の項目であったとおりPyGraphvizを用いていますので、PyGraphviz経由でのファイル保存や画像表示となります。

* ファイル保存する場合
    ファイルに保存する場合は以下のコードを実施します。これによりfilenameにファイルが保存されます。

    ```Python
    filename = 'test.png'
    model.get_graph().draw(filename, prog='dot', format='png')
    ```

* Jupyter Notebook上に表示する場合
    Jupyter Notebookを使っていれば以下のコードにより実行結果をNotebook上に表示させることが可能です。

    ```Python
    import io
    from IPython.display import Image, display

    stream = io.BytesIO()
    model.get_graph().draw(stream, prog='dot', format='png')
    display(Image(stream.getvalue()))
    ```

#### Windowsの場合 (PyGraphvizをインストールしていない場合)
Windowsでは「GraphvizとPyGraphvizのインストール」の項目における方法①に関して説明します。
こちらはGraphviz経由でのファイル保存や画像表示となります。

まずファイル保存/画像表示の共通操作として、描画するために必要なGraphvizのDigraphオブジェクトを取得します。
このDigraphオブジェクトは「遷移等を行って再描画する必要がある」度に取得する必要がある点に注意してください。

```Python
dg = model.get_graph().generate()       #オブジェクトの取得
```

* ファイル保存する場合
    ファイルに保存する場合は以下のコードを実施します。これによりfilenameにファイルが保存されます。

    ```Python
    filename = 'test'                   #こちらは拡張子が自動付与されるはず
    graphviz.Source(dg, filename=filename, format='png').render(cleanup=True) 
    ```

* Jupyter Notebook上に表示する場合
    Jupyter Notebookを使っていれば以下のコードにより実行結果をNotebook上に表示させることが可能。

    ```Python
    import graphviz
    from IPython.display import Image, display

    display(Image(dg.pipe(format='png'))) 
    ```

### GraphMachineの実装例（GraphMachineを便利に使う）
実際にGraphMachineで実装してみると分かると思いますが、状態が変わるごとにJupyter Notebook上などで毎回描画更新用のメソッドを手動で実施させるのは面倒なので、以下のようにコールバック関数に登録してしまうと良いかもしれません。
GraphMachineは動作確認用等で使うと思いますが、この辺りはお好みで実装してもらえればと思います。
Jupyter Notebook上で以下のコードを実行すると、実行結果に状態遷移図が表示されると思います。

簡単にコードの説明をしておきますと、以下のコードで共通で使われているfinalize_eventは、遷移状態に関係無く何らかのトリガー(イベント)がステートマシンに与えられた際に実施されるコールバックの指定になります。この辺りもまた別の記事で紹介したいと思いますので詳細は割愛します。
また以下のサンプルコードにおいて、modelオブジェクトを作成する際のMatterクラスにファイル名を入力すると、状態遷移画像を画面表示ではなくファイルとして保存されるようにしてあるので、Jupyter Notebook上以外で確認したい場合などにはファイル指定すると良いと思います。

#### Linux/macOSの場合 (PyGraphvizをインストールしている場合)
こちらはLinux/macOS版でのGraphMachine実装例になります。

```Python
import io
from IPython.display import Image, display
from transitions.extensions import GraphMachine

states = ['A', 'B', 'C']        #状態の定義

class Matter(object):
    def __init__(self, filename=None):
        if (filename is None):
            self.output = io.BytesIO()
        else:
            self.output = filename

    def action_output_graph(self, *args, **kwargs):
        self.get_graph(*args, **kwargs).draw(self.output, prog='dot', format='png')
        if isinstance(self.output, io.BytesIO):
            display(Image(self.output.getvalue()))
            self.output.seek(0)
            
		
model = Matter()                #ファイル出力する場合は、Matter('test.png')にする。
machine = GraphMachine(model=model, states=states, initial='A', 
                       auto_transitions=False, ordered_transitions=True,
                       finalize_event='action_output_graph',
                       title='', show_auto_transitions=False, show_conditions=False)	

model.action_output_graph()     #初回のみ手動実施
```

実際に上記コードをmacOS上及びLinux（ubuntu）とJupyter Notebookを使って動かしてみたところ、以下の出力が得られました。これは当記事最初の動作確認コードをGraphMachineを用いて描画したものとなります。
![transitions_circle.png](https://qiita-image-store.s3.amazonaws.com/0/291448/4e49c6f3-f0cb-40c3-89ee-2b91461b9033.png)

上記コード実行の後、動作確認コード同様にmodel.next_state()を実行していけば、実行のたびに画像が再生成され、状態が遷移していくことが確認できるはずです。

#### Windowsの場合 (PyGraphvizをインストールしていない場合)
こちらはWindows版でのGraphMachine実装例となります。
細かいことは除いてLinux/macOS版(PyGraphviz使用版)と同様の動きとなります。

```Python
import graphviz
from IPython.display import Image, display
from transitions.extensions import GraphMachine

states = ['A', 'B', 'C']        #状態の定義

class Matter(object):
	def __init__(self, filename=None):
		self.output = filename
			
	def action_output_graph(self, *args, **kwargs):
		dg = model.get_graph(*args, **kwargs).generate()
		if isinstance(self.output, str):
			graphviz.Source(dg, filename=self.output, format='png').render(cleanup=True) 
		else:
			display(Image(dg.pipe(format='png'))) 
		
model = Matter('test')         #ファイル出力する場合は、Matter('test')にする。
machine = GraphMachine(model=model, states=states, initial='A', 
                       auto_transitions=False, ordered_transitions=True,
                       title='', show_auto_transitions=False, show_conditions=False, 
					   finalize_event='action_output_graph')	

model.action_output_graph()     #初回のみ手動実施
```

上記コードをWindows10(64bit)のJupyter Notebook上で動作させてみたところ、以下のグラフが生成されました。
Linux/macOS版(PyGraphviz使用版)の結果と違い、角の取れた四角いノードの状態遷移図が生成されますがステートマシンとしての動作は同じです。これは画像を生成するGraphvizを使用していますが、その際の描画設定がLinux/macOS版(PyGraphviz使用版)と変更されているためです。実際はGraphMachineによりDigraphオブジェクトが生成された後に形状を変更できますが今回は詳細割愛します。

![transitions_rect.png](https://qiita-image-store.s3.amazonaws.com/0/291448/7ae62f07-32d0-a96d-98e2-8b26c37dc387.png)

# まとめとあとがき
結局のところステートマシンの定義と状態遷移の動きを実装し動かすだけであれば、transitionsのMachineクラスのみで良いです。（OSに関係なく動くはず）
一方で設計したステートマシンの動きを状態遷移図で確認したり、状態遷移の勉強や当パッケージの理解を深めるためにGraphMachineを利用することも有用なので、その辺りはうまく使い分けていただけたらと思います。
もちろんGraphMachineは状態遷移画像を生成するため、ステートマシン動作が同じでMachineクラスで作られたものに比べ遅くなるのは言うまでもないので、RaspberrPiなどに組み込む場合は留意いただければと思います。

準備編といいつつGraphMachineの説明のところでかなり長くなってしまいました。
これもPyGraphviz/Windowsのバグによる混乱がありますが、今後のバージョンアップで改善が見込まれているので導入も簡単になると思います。多分GraphMachineのバックエンドがGraphvizに統一されれば今回紹介した「Windowsの場合」のインストール方法や実装方法に近くなるとは思います。

長々とここまで読んでくれた方、ありがとうございました。