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

trantisionsはPythonで状態遷移を実現するためのパッケージですが、今回は状態遷移を実現するために最も基本となる「状態」の定義について紹介したいと思います。
遷移の定義やトリガー（遷移の元となるイベント）、遷移時のイベントについては別の記事で紹介します。実のところユーザが定義する遷移(Machineクラスに設定するtrantisions）を設定しないで状態遷移できる仕組みもありますが、それは別の記事で紹介します。

#この記事の対象者と今回の内容
Pythonで状態遷移を実装したり動作確認をしたい方に、Pythonの状態遷移パッケージ「transitions」の使い方を説明していきたいと思います。状態遷移そのものは組込みとか制御などでよく使われるものですが、それをPythonで実現したい場合にこのパッケージが有用かと思います。
今回は状態遷移において重要な「状態」(State)に関して詳細を説明します。
その他、transitionsの概要やインストール方法、グラフ表示機能の設定については[準備編](https://qiita.com/noca/items/53c01ac5afb687a98db8)の記事を参照頂けたらと思います。
それでは、早速はじめていきましょう。

#状態の定義について
transitionsはMahineというクラスに遷移やら状態、イベントなどを指定してステートマシンを作るのですが、状態遷移というからにはまず「状態」を定義します。
状態の定義としては基本的に以下のようにリストと文字列で指定するのが最も簡単な指定方法になります。

```Python
states = ['A', 'B', 'C']
```

上記では、A、B、Cという3状態を定義しています。
その他にもStateクラスや辞書を使って定義することも可能です。

```Python
states = [State(name='A'), State(name='B'), State(name='C')] #Stateクラスで指定
states = [{'name': 'A'}, {'name': 'B'}, {'name': 'C'}]       #辞書で指定
```

上記statesはどれも一緒の意味合いになります。補足ですが、これら文字列、Stateクラス、辞書は混在させることも可能です。

```Python
states = ['A', State(name='B'), {'name': 'C'}]              #混在させて指定
```

ひとまず状態の定義が済みましたのでとりあえずこれをステートマシンに登録してみましょう。
状態の定義であるstatesも含めたサンプルコードが以下になります。

```Python
from transitions import Machine

states = ['A', 'B', 'C']

class Model:
    pass

model= Model()
machine = Machine(model=model, states=states, initial=states[0], 
                  auto_transitions=False, ordered_transitions=False)
```

細かな引数はおいといて、まずはMachineクラスの各引数（model, states, initial）に注目ください。
model引数はステートマシン設定先となるクラスのインスタンスになります。ここではModelクラスのインスタンスであるmodelオブジェクトが指定されています。
states引数はステートマシンの状態となります。ここでは定義されたstetesリストが指定されています。
initial引数は一番初期の状態を定義しています。ここではstates[0]、つまり状態'A'が初期の状態になっています。

これはつまり、ステートマシンを作成するにはステートマシンの設定先となるクラス（下記例では空のModelクラス）と、設定クラスであるtrantitions.Machineクラスを使います。
定義された「状態」はこのMachineクラスのstates引数に設定し、Machineクラスを通じて設定先のクラス（上記例ではModelクラスのインスタンスであるmodel）に設定されます。

ちなみに、Machineクラスによって設定されたmodelオブジェクトの状態を表すと以下のようになります。
※以下はMachineクラスではなくGraphMachineを使って描画したものになります。また、通常「状態」のみのグラフを作ると各状態は縦方向に配置されますが、表示の都合上GraphMachineの設定を変更し横方向に並べています。
![DefinedState_1.png](https://qiita-image-store.s3.amazonaws.com/0/291448/ea60a1c6-b4ea-2369-1b7c-cf340d07a5f9.png)

## 状態に関する設定
Machineクラスに指定できる状態(states)において、名前以外にいくつか状態に紐付く設定を行う事ができます。
状態名だけの設定であれば冒頭の例のようにlistと文字列のみで良いですが、以下のような設定を行う場合はStateクラスもしくは辞書を用いてそれぞれ引数もしくはkeyとして設定できます。

|        項目       |         引数/key        |   型   |      初期値      |              説明              |
|:------------------|:------------------------|:-------|:----------------|:-------------------------------|
| 状態名            | name                    | str    | －(必ず指定する) | 状態の名称。初期化や状態の設定、GraphMachineでの表示内容もここで指定された文字列を使う |
| enterコールバック | on_enter                | list   | None            | 状態に入った時に実施されるコールバック |
| exitコールバック  | on_exit                 | list   | None            | 状態を出た時に実施されるコールバック  |
| 無効イベント無視  | ignore_invalid_triggers | bool   | False           | 状態に定義されていないイベントトリガーの無視 |

on_enterは状態に入った際に実行されるコールバックで、on_exitは状態から出た際のコールバックになります。一般的にはMachineに設定するmodelのメンバーメソッド名をstr型で設定することになります。詳細は別の記事で紹介しますが当設定により、イベント(トリガー)により遷移してきた場合、イベント(トリガー)によらずコールバックを実施することができます。
またignore_invalid_triggersに関しては定義外のイベントが発生した際に例外を起こさない設定になります。各状態それぞれで設定出来る項目であり、transitionsパッケージでは、通常各状態において定義されていないトリガー(イベント)が与えられた場合は例外を発生する仕組みになっていますが当設定をTrueにすることで例外を発生しないようにできます。遷移に関連が深いので詳細は遷移編で紹介します。
状態に関する設定例は以下のような形となります。

```Python
from transitions import State
states = [
	'A',												# str型による設定例(状態Aの設定)
    State(name='B', on_exit=['action_good_bye']),		# Stateクラスでの設定例 (状態Bの設定)
    {'name': 'C', 'ignore_invalid_triggers':'True'},	# 辞書での設定例 (状態Cの設定)                                                  
]
```

冒頭の例にあるとおり、Machineクラスによってmachineオブジェクトを作成する際、上記で定義されたstatesリストをMachineクラスのstates引数に指定すれば、machineオブジェクトにstetesリストで定義された「状態」が与えられます。また同時にMachineクラスのmodelに指定したオブジェクトにおいても、現在の「状態」の確認などの操作が行えるようになります。

```python
# statesで定義された「状態」がmachineとmodelオブジェクトに付与される。
machine = Machine(model=model, states=states, initial=states[0], 
                  auto_transitions=False, ordered_transitions=False)
```

## 状態に関する操作
状態に関する操作としては、状態の確認、設定、追加が挙げられます。
状態の削除に関しては無く、状態を削除したい場合は新たにmachineを定義することになります。なお、状態の総合的な管理はMachineクラスで作成されたmachineオブジェクトが行っているため、machineオブジェクトで操作することが主となりますが、ステートマシンが付与されるmodelオブジェクトでも一部状態に関する操作が行えます。
また以下表のstate以外は全てメソッドになります。

| メソッド/変数 | 対象オブジェクト | 戻り値 |     引数       |                                          説明                                    	|
|:-------------|:-----------------|:-------|:--------------|:-----------------------------------------------------------------------------------|
| state        | model            | str    |       －      | modelの現在の状態確認。メソッドではなく変数						|
| is_状態名    | model            | bool   |       －      | 現在の状態が'状態名'と同じが判定する<br>(True：同じ, False：異なる)			|
| is_state　   | machine          | bool   | model, state  | 引数modelが引数stateと同じが判定する<br>(True：同じ, False：異なる)			|
| get_state    | machine          | State  | state         | 引数stateの状態を取得する。modelは指定不可                                  	|
| state        | machine          | State  |       －      | machineに現在設定されている全ての状態をStateクラスで返す。メソッドではなく変数	|
| set_state    | machine          | －     | state, model  | machineに状態を設定する。modelは省略可能。<br>その場合全てのmodelに追加される	|
| add_states   | machine          | －     | Stateと同じ   | machineに状態を追加する。メソッド名はadd_stateでも可                         	|

以下、状態に関する各操作についての詳細と例を示します。

#### 状態の確認
状態の確認としてはmodel.is_状態名で現在の状態を確認することができます。

```Python
>>> model.is_A()
True
>>> model.is_C()
False
```

さらにmachineオブジェクトとis_stateを用いて確認することができます。繰り返しになりますがmachineオブジェクトはステートマシンの総合的な管理をしており、1つ以上のmodelを扱う可能性もあるので引数にはmodelの指定が必要な点にご注意ください。またこちらは戻り値がbool型になります。
冒頭の例のとおり、ひとまずmodelオブジェクトの現在の「状態」が'A'という前提として例を示します。

```Python
>>> machine.is_state('A', model)
True
>>> machine.is_state('C', model)
False
```

#### 状態の取得
状態の確認に関してはmodelおよびmachineそれぞれで行えます。
既に何度も出ていますが、現在操作しているmodelの状態を確認するには以下の通りstateを使います。
ひとまず現在の状態が'A'という前提とした場合の例がこちらになります。戻り値はstr型になります。
なおMachineクラス内で自身の現在の状態を確認したい場合はself.stateで確認出来ます。（結果は以下と同じです）

```Python
>>> model.state
'A'
```

次にmachineオブジェクトを用いた状態の取得についてになります。こちらはmachineに設定された状態(State型)を取得できます。
あくまでもMachineクラスで生成されたmachineオブジェクトに対して行う事ができ、ステートマシンの付与されたmodelには当操作が行えないことにご注意ください。戻り値はState型になりますので、Stateのメンバ変数を用いて設定内容を確認することができます。
以下のコードではget_stateメソッドでmachineオブジェクトから状態を取得し、それら状態のメンバと設定値を取得しています。冒頭のコード例において、状態'A'に対して状態を取得する場合の例は以下になります。

```Python
#machineに定義されたstateの情報を取得する
state = machine.get_state('A')
state_val = state.__dict__
for member in state_val.keys():
	print('{} : {}'.format(member, state_val[member]))
```

実行結果は以下の通りとなります。

```python
name : A
ignore_invalid_triggers : None
on_enter : []
on_exit : []
```

#### 状態の設定
こちらはmachineオブジェクトに状態(State)を設定するメソッドになります。
状態の初期設定はmachineオブジェクト生成時のMachineクラスのinitial引数にて行う事になり、状態の変更は定義された各種イベント(トリガー)によって変更することなりますが、このset_stateメソッドにおいても状態を設定することが可能です。また、Machineに設定された個々のmodelに対して設定可能です。同時にこれは遷移を設定していない場合においても状態を変更できることを意味しています。ただしこの場合、遷移が伴わないので各種コールバックは発生しません。
以下コード例になります。

```python
>>> machine.set_state('B')
>>> model.state
'B'
```

GraphMachineについての補足ですが、当メソッドによって状態を変更してグラフを再描画する場合はGraphMahchineのグラフオブジェクト取得メソッドであるget_graphに対し、引数force_new=Trueを与えて下さい。これを行わないと再設定後の状態が更新されません。
前回の[準備編](https://qiita.com/noca/items/53c01ac5afb687a98db8)の実装例を踏襲する場合はset_state後に以下のコードを追加して実行すれば状態再設定後のグラフを生成できます。

```python
model.action_output_graph(force_new=True)
```

実際にグラフ表示すると以下になります。繰り返しになりますが、これはイベント等の遷移による結果ではないことにご注意ください。
![test.png](https://qiita-image-store.s3.amazonaws.com/0/291448/d918f961-2a14-b5dd-10c0-beb32e155f9a.png)


#### 状態の追加
こちらはmachineに状態(State)を追加するメソッドになります。こちらはMachineクラスに設定された全てのmodelに対し設定される事になります。
以下は冒頭のコード例について状態'D'を追加した例になります。

```python
>>> machine.add_states('D', on_enter=None, on_exit=None, ignore_invalid_triggers=None)
>>> model.state
'A'
```

GraphMachineでグラフ表示すると以下の通りになります。

![test.png](https://qiita-image-store.s3.amazonaws.com/0/291448/ef576581-d0a8-a2dc-eb80-cdf1c5731fd8.png)

結果を見ると分かる通り、状態を追加しただけなので冒頭のコード例について上記の通り状態追加(add_states)を実行しても、現在の状態が'A'から変化していないことが確認できると思います。
GraphMachineについて補足すると、こちらもset_state同様にグラフの描画についてはget_graphに対して引数force_new=Trueを与えてください。

# まとめ
transitionsにおいて、状態にはそれぞれ状態名、状態遷移前後のコールバック、設定外トリガー実施時の例外発生有無の設定ができ、状態の取得や確認、設定、追加が行えることが確認出来たかと思います。
一方で状態を設定したのみでは状態を遷移することができず、状態遷移時のコールバックも発生しないことからステートマシンとしての動作としては不十分です。特に今回紹介したコードでは遷移を行う事ができません。そこで次回はステートマシンとして動作するために必須である遷移について詳細説明したいと思います。