Python
ステートマシン
状態遷移
transitions

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

transitionsはPythonで状態遷移を実現するためのパッケージですが、今回は状態遷移を実現するために「遷移」について紹介したいと思います。

この記事では状態遷移を実現するソフトウェア的な機構をステートマシンと呼んでいます。


この記事の対象者と今回の内容

Pythonで状態遷移を実装したり動作確認をしたい方に、Pythonの状態遷移パッケージ「transitions」の使い方を説明していきたいと思います。状態遷移そのものは組込みとか制御などでよく使われるものですが、それをPythonで実現したい場合にこのパッケージが有用かと思います。

今回は状態遷移において重要な「遷移」(transitions)に関してですが「遷移編2」として、ユーザが遷移情報を設定しないで状態に対し自動的に付与するユーザ定義外の遷移について紹介します。

ユーザ定義の遷移についてや、コールバック/ブロック判定、遷移に対する操作などは遷移編1をご確認頂けたらと思います。

その他、transitionsの概要やインストール方法、GraphMacineによるグラフ表示機能の導入や設定については準備編の記事を参照頂けたらと思います。

※今回の記事でも公式チュートリアルにならい遷移をtransitionsと呼んでますが、パッケージ名のtransitionsとややこしいので、当パッケージそのものを示す場合はtransitionsパッケージと明示することにします。


遷移定義をしない場合

状態や遷移についての定義は遷移編1にて言及したので割愛しますが、当transitionsパッケージにおいて、遷移を定義しないで状態のみの最小限の設定でステートマシンを定義した場合はどうなるでしょうか?

以下のコードで確かめてみます。


Machineに最小限の設定をした場合の状態遷移の定義例

from transitions import Machine

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

class Model(object):
pass

model = Model()
machine = Machine(model=model, states=states, initial=states[0])


概ね、状態のみを定義した最小限のステートマシンで、Machineクラスの引数も動作するほぼ最小限の設定を入れています。

多分transitionsパッケージを導入した方は最初にこんな定義を入れるのが簡単かと思いますが、普通に考えたら「これでは状態のみが定義されただけで状態遷移できないのでは?」と考えると思います。現に状態編では似たような定義を行い、状態のみしか生成されていないことを示しています。

一方で上記コードを実行してイベントを確認した場合が以下のようになります。

初めてtransitionsパッケージを触るユーザの期待値としては遷移(transitions引数)を設定していないので遷移に紐付くイベントも無いと予想すると思います。

>>> [event_name for event_name in  machine.events.keys()] # ステートマシンに存在するイベントを確認

['to_A', 'to_B', 'to_C']

イベントを確認した結果、なぜかユーザ自らが定義していないイベントがあることが確認できました。

GraphMachineでも状態遷移図を描画して確認してみます。

image.png

「あれ?イベントや遷移を定義してないのにイベントと遷移がある・・・怖い」と混乱するかもしれませんがこれにはカラクリがあり、これがユーザ定義外の遷移になります。

※本当に遷移やイベントを付与せず状態のみを定義したい場合はこの後の説明ででてきますが、状態編でも紹介していますので適宜ご参照ください、


ユーザ定義外の遷移

前振りが長くなりましたが、上記例のようにユーザが遷移(transitions辞書等)を設定しなくてもMachineに対する設定で遷移を状態に付与することができます。

これを当記事ではユーザ定義外の遷移と読んでいますが、大きく分けて「全状態遷移」と「順序遷移」があります。これらの初期設定と有効化/無効化、および細かな設定についても紹介したいと思います。なお、状態編遷移編1ではこれらユーザ定義外の遷移が付与されないように設定しています。


全状態遷移

冒頭の実施例のカラクリ(自動的に付与された遷移)はこちらの設定による影響になります。

全状態遷移(auto_transitions)はmachineオブジェクト生成時のMachineクラスに指定するもので、デフォルトではTrue(全状態遷移=有効)となっています。

この全状態遷移での遷移方法はto_状態名()となっています。冒頭の実施例に対し遷移を実行してみます。


Machineに最小限の設定をした場合の状態遷移の遷移例

>>> model.state     # 現在状態の確認

'A'
>>> model.to_C() # 状態Cへのトリガーイベント
True
>>> model.state # 遷移後状態の確認
'C'

遷移の状況をGraphMachineで出力した例が以下になります。

状態Aから状態Cへto_Cトリガーイベントを通じで遷移している事が確認できます。

image.png

この例から分かる通り、to_状態で現在の状態から任意の状態へ遷移できるのかこの全状態遷移になります。

この全状態遷移を使わない場合は、machineオブジェクト生成時にMachineクラスに対して明示的にauto_transitions=Falseにする必要があります。


全状態遷移設定を無効にした場合の定義例

from transitions import Machine

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

class Model(object):
pass

model = Model()
machine = Machine(model=model, states=states, initial=states[0])


これで「ユーザが遷移を定義していない」という想定通りの定義になりました。

image.png

ただ、当然のことながらこのステートマシンでは遷移とイベントを定義していないので、状態を遷移することはできません。

"状態を変える"だけであれば状態編で示したとおり、set_stateメソッドを使って状態を変更することはできます(ただしset_stateメソッドは状態を変えるだけであって遷移は伴わないので各種コールバックは起こりません)。

ちなみにユーザ定義外の遷移といっていますが、この全状態遷移(auto_transitions)はユーザ定義遷移を実装した場合(transitions辞書での設定やadd_transition(s)などでの遷移追加)と合わせて使う事は可能です(詳細は補足を参照のこと)。

しかしながら、この全状態遷移は全状態に任意で遷移できてしまうことから思わぬバグを生む可能性があるので、基本的に(ユーザ定義)遷移を付与する場合は無効にしておくことを強く勧めます。

あくまでも動作確認やデバッグ程度(状態に関するコールバックの確認など)で使うのであれば良いかとは思います。


順序遷移

次に順序遷移(ordered_transitions)になります。

こちらも同様にmachineオブジェクト生成時のMachineクラスで設定でき、ユーザが遷移設定をしなくても付与できる遷移になります。ただしこちらはデフォルトでは無効になってるので使用する場合は明示的にordered_transitions=Trueにしてやる必要はあります。

以下、順序遷移を有効にした場合の定義になります。

なお、この順序遷移(orderde_transitions)は全状態遷移(auto_transitions)と併用が可能なので、当定義例では全状態遷移は無効にしてあります。


順序遷移を有効にした場合の定義例

from transitions import Machine

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

class Model(object):
pass

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


上記コードをGraphMachineで状態遷移図を作成すると以下のようなグラフが得られます。

image.png

状態遷移図からも分かる通り、順序遷移を有効にすると自動的にnext_stateというトリガーイベントが各状態に付与されます。

実際にnext_stateで遷移できるか確認してみましょう。


Machineに最小限の設定をした場合の状態遷移の遷移例

model.state

'A'
model.next_state()
True
model.state
'B'

遷移の状況をGraphMachineで出力した例が以下になります。

image.png

さて、ここで「next_stateのnextとは何だろう」という一つの疑問が生まれると思いますが、これは状態定義(states)に定義した順がnext(次の状態)になります。

従って状態(states)を['A', 'C', 'B']と定義すると以下のような遷移となります。状態Aでnext_stateメソッドを実施した時、次の状態が状態Cになっていることを確認できると思います。

image.png

なおこの順序遷移についてもユーザ定義遷移を実装した場合と合わせて使う事が可能です(詳細は補足で説明します)。


順序遷移の追加

順序遷移については、全状態遷移と異なりmachineオブジェクト生成後においてもadd_ordered_transitionsメソッドを用いて付与することが可能です。

前述の順序遷移と全く同じ定義をadd_ordered_transitionsメソッドを用いて実現してみます。


add_ordered_transitionsによる順序遷移の付与

from transitions import Machine

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

class Model(object):
pass

model = Model()
machine = Machine(model=model, states=states, initial=states[0], auto_transitions=False)
machine.add_ordered_transitions() #順序遷移の付与


ちなみにadd_ordered_transitionsを使って順序遷移を追加する場合の注意点として、Machineクラスの引数で設定するordered_transitionsは有効にしてはいけないという点です。

理由としてはtransitionsパッケージは個別追加された(全く同じ遷移元で全く同じイベントによる)遷移については例外が発生せず追加されてしまうので、Machineクラスで順序遷移を定義し、さらにadd_ordered_transitionsで詳細設定せず遷移を追加した場合はnext_stateが多重定義される恐れがあります。もちろん後述するtrigger名を変更すれば問題ありませんが、add_ordered_transitionsメソッドで順序遷移を追加する場合はご注意下さい。


順序遷移の詳細設定

順序遷移をMachineクラスのordered_transitionsで有効にした場合は上記の通りの動作となり細かな設定は行えませんが、add_ordered_transitionsメソッドを用いて順序遷移に対する細かな設定を行う事が可能です。

まずはadd_ordered_transitionsメソッドの引数で設定できる項目の一覧について紹介します。

下表において、ガード判定とコールバックの設定については遷移編1に詳細説明しており、挙動も変わらないのでここでは説明を割愛します。

一方でadd_ordered_transitionsメソッドにおける①付与する状態、②トリガーイベント、③ループ有無、④初期状態のループ設定については独自項目になるので順次詳細説明したいと思います。

項目
引数

初期値
説明

付与する状態
states
None/list
None
順序遷移を付与する状態を指定する
Noneの場合は現在定義済みの全状態(stetes)に付与する
listで個別指定する場合は少なくとも2状態の指定が必要

トリガーイベント名
trigger
str
'next_state'
次状態への遷移を起こすトリガーイベントのメソッド名

ループ有無
loop
bool
True
順序遷移をループ(終点がない遷移)にするか
True:ループにする, False:ループにしない

初期状態のループ設定
loop_includes_initial
bool
True
順序遷移がループする際、初期状態をループに含めるか
True:含める, False:含めない

ガード判定(True)
conditions
str/List
None
指定されたコールバックがTrue時に遷移を許可
詳細は遷移編1のブロック判定 を参照

ガード判定(False)
unless
str/List
None
指定されたコールバックがFalse時に遷移を許可
詳細は遷移編1のブロック判定 を参照

遷移準備コールバック
prepare
str/List
None
triggerが発生した際に実行されるコールバック
詳細は遷移編1のコールバック を参照

遷移前コールバック
before
str/List
None
状態が遷移する前に呼び出されるコールバック
詳細は遷移編1のコールバック を参照

遷移後コールバック
after
str/List
None
状態が遷移した後に呼び出されるコールバック
詳細は遷移編1のコールバック を参照

以下①~④について順序遷移の独自設定について紹介します。

※以下の説明において、add_transitionsの内容以外は「add_ordered_transitionsによる順序遷移の付与」のコード例と変わらないので、add_ordered_transitionsの引数を以下に変更して実行してみると同様の結果が得られます。

①「付与する状態」の設定

引数statesに関する内容になり、こちらは順序遷移を付与する状態をlist形式で指定する形になります。指定した2状態以上に対し順序遷移(順序遷移のトリガーイベントと遷移)を割り当てます。

引数の指定は必須ではなく、指定しない場合stateはNoneとなり、この場合は現在定義されている全状態に対して順序遷移を付与する形になります(順序遷移デフォルトの動作)

Noneを指定した場合は前述の例と変わらないので、まず3状態において2状態のみ順序遷移を割り当てる例を示します。

machine.add_ordered_transitions(states=['A', 'B'])

上記をGraphMachineで描画すると以下の通りとなります。

付与する状態(states)を設定しない場合(states=None)では全状態に順序遷移が割り振られていましたが、こちらは指定した状態A~状態B間に順序遷移が割り振られている事が確認できます。

image.png

補足:順序遷移で自己遷移を実現する場合

なおこの「付与する状態(states)」に指定できる状態は2状態以上ですが、['A', 'A']とすることで自己遷移に対する順序遷移を付与可能です。

ただし後述するloop=falseを入れておかないと、遷移が「next_state|next_state」と二重定義になってしまうので注意が必要です。

machine.add_ordered_transitions(states=['A', 'A'], loop=False)

上記をGraphMachineで確認してみると以下を得られます。

image.png

またこの方法は複数の状態に対し行う(例えば状態Aと状態Bをそれぞれ二重に指定する)と状態遷移は作れますが、「遷移元から同じイベントで複数の遷移が生じてしまう」等の想定外の動作になりますのであまりオススメはできません。

一応やり方としては存在するのでご紹介まで。

②「トリガーイベント名」の設定

引数triggerに関する内容になります。

こちらは順序遷移を行うトリガーイベント名の指定であり、指定しない場合はデフォルトで'next_state'という文字列が順序遷移におけるトリガーイベント名になります。

以下、トリガーイベントの設定を変更した例を示します。

machine.add_ordered_transitions(trigger='Yeahhhhhhhhhhhhh')

上記をGraphMachineで描画してみると、next_stateの代わりに定義したトリガー名がトリガーイベントになっていることが確認できると思います。

image.png

③「ループ有無」の設定

引数loopに関する内容になり、こちらは順序遷移が一巡するかをどうかをboolで設定するものになります。

一巡とはどういうことかというと、statesの定義が['A', 'B', 'C']とあった場合、順序遷移におけるトリガーイベントを起こす度に状態A → 状態B → 状態Cという順に遷移します。

この際、ループ設定が有効(loop=True時)ではstateの最終定義されている状態Cから、状態C → 状態A → 状態B → ・・・と、状態が初期状態Aに戻り、遷移が続きます。

従ってトリガーイベントを実行する度に定義した状態への遷移が順次繰り返される事になります。

一方で、ループ設定が無効(loop=False時)では状態Cへ行ったところでそれ順序遷移におけるトリガーイベントでは遷移できない形になります。

デフォルトの設定ではループ設定が有効になっているので、無効にした場合の例を見てみます。

machine.add_ordered_transitions(loop=False)

こちらを図示すると一目瞭然だと思います。

image.png

④「初期状態のループ設定」の設定

引数loop_includes_initialに関する内容で、こちらは順序遷移が一巡する場合に初期状態をループに含めるか否かの設定になります。

つまり順序遷移のトリガーイベントでループはするけど初期状態へはループしない(戻らない)というものになります。

初期状態のループ設定が有効(loop_includes_initial=True)では初期状態が含まれ、無効((loop_includes_initial=False)では含まれません。

デフォルト設定では「初期状態のループ設定」が有効になっているので、無効にした例をみてみます。

machine.add_ordered_transitions(loop_includes_initial=False)

こちらを図示すると以下の通り、順序遷移トリガーイベントであっても、初期状態である状態Aからは抜ける遷移しかなく、状態A以外の状態においてループしていることが確認できると思います。

なお、この設定を行うにはループ設定を有効(loop=True)にしておく必要があり、ループ設定が無効になっているとその設定が優先されます(「③「ループ有無」の設定」の無効例と同様の遷移)

image.png


ユーザ定義外遷移に関する設定

上記までのユーザ定義外遷移に関する内容をまとめると以下の通りになります。

以下表の内容は全てMachineクラスの引数に設定する内容です。

前述の通り順序遷移(ordered_transitions)の設定はboolであり細かな設定はできません。順序遷移に対し細かな設定を行う場合はmachineオブジェクト作成後add_ordered_transitionsメソッドで順序遷移を追加ください。また全状態遷移への設定は当項目以外にはなく、全状態遷移にて紹介した内容が概ね全てとなります。

項目
引数

初期値
説明

全状態遷移
auto_transitions
bool
True
全状態に対し任意に遷移できる遷移設定を付与する
True:する, False:しない

順序遷移
ordered_transitions
bool
False
全状態に対し順番に遷移できる遷移設定を付与する
True:する, False:しない


ユーザ定義外遷移に関する操作

こちらも前述の内容のまとめになります。

ユーザ定義外遷移に関する操作としては、ユーザ定義外遷移の操作、順序遷移の追加が挙げられます。

メソッド
対象オブジェクト
戻り値
引数
   説明

to_状態名
model
bool
通常なし※2

to_状態名()で指定状態への遷移を行うイベントを起こす※3

next_state※1

model
bool
通常なし※2

次状態への遷移するためのトリガーイベントを起こす※4

add_ordered_transitions
machine

順序遷移の章参照
machineに対し順序遷移を追加する
ordered_transitions設定と異なり
順序遷移に対し詳細設定が可能

※1:add_ordered_transitionsのtrigger引数で変更可能

※2:コールバックにデータを渡す場合は指定する場合もありますが、詳細はコールバック編に記載します。

※3:全状態遷移が有効時(auto_transitions=True時)のみ利用可能

※4:順序遷移が有効時(ordered_transitions=True時もしくはadd_ordered_transitions実行済み時)のみ利用可能


補足

本題ではないものの、いくつか重要な説明を補足します。


ユーザ定義外遷移とユーザ定義遷移の同時定義について

ユーザ定義外遷移といいつつ、実際は当記事で紹介した遷移とユーザ定義遷移(transitions辞書などによる遷移)は全て併用可能です。

例えば状態A、B、Cにそれぞれ独自のユーザ定義遷移を定義した上でユーザ定義外遷移である全状態遷移と順序遷移の設定を追加してみます。

from transitions import Machine

states = ['A', 'B', 'C'] #状態の定義
transitions = [
{'trigger':'fromAtoB', 'source':'A', 'dest':'B'},
{'trigger':'fromBtoC', 'source':'B', 'dest':'C'},
{'trigger':'fromCtoA', 'source':'C', 'dest':'A'},
]
class Model(object):
pass

model = Model()
machine = Machine(model=model, states=states, transitions=transitions, initial=states[0],
auto_transitions=True, ordered_transitions=True)
machine.add_ordered_transitions(states=['A', 'B', 'C']) #順序遷移の付与

念のため定義されたイベント一覧を確認してみます。

>>> [event for event in machine.events]

['to_A', 'to_B', 'to_C', 'fromAtoB', 'fromBtoC', 'fromCtoA', 'next_state']

既に嫌な予感がしますが、GraphMachineでグラフ描画してみます。

image.png

このように、ユーザ定義外の遷移とユーザ定義の遷移とを合わせられることが確認できましたが、非常に蛇足的な状態遷移ができていることが理解できると思います。

特に状態A → 状態B、状態B → 状態C、状態C → 状態Aといった遷移は3つのトリガーイベントが被っているので非常に冗長です。

更にこう複雑になってくると既に状態遷移図のみで把握するのは難しくなってくるので、こういう複雑な状態遷移を設計する場合は状態遷移表は不可欠になると思います。

このような定義の仕方であっても、もちろん小規模な状態遷移の実現やデバッグ等の用途では使える場面もあるかもしれませんが、できる限りシンプルな状態遷移を設計することがバグを生まない良い設計になるのでその辺りを意識して実装されると良いかと思います。


GraphMachineによる全状態遷移の描画について

本編ではあまりGraphMachineについて触れませんでしたが、ユーザ定義外遷移の全状態遷移においてGraphMachineの設定があるので、そちらについて補足します。

GraphMachineを用いた全状態遷移に対する状態遷移の簡単な定義例としては以下のようになものが挙げられます。

※以下の例は、グラフ出力以外は本記事最初の「Machineに最小限の設定をした場合の状態遷移の定義例」と全く同じ動作をするものです。


GraphMachineを用いた全状態遷移のグラフ出力例 (transitions0.69/Linux/PyGraphviz版)

from transitions.extensions import GraphMachine

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

class Model(object):
pass

model = Model()
machine = GraphMachine(model=model, states=states, transitions=transitions, initial=states[0],
auto_transitions=True, ordered_transitions=False, title='')
model.get_graph().draw('transitions.png', prog='dot', format='png') # ファイルで出力


実際にこのGraphMachineが出力したmachineに設定されているイベント一覧を取得してみます。

>>> [event for event in machine.events]

['to_A', 'to_B', 'to_C']

きちんと全状態遷移のイベントが設定されているようにみえます。

一方で上記コードで出力した画像を見てみると、以下の通り全状態遷移が描画されていません。

image.png

これはバグではなく、GraphMachineにおいてデフォルト設定では全状態遷移(auto_transitions=True)については表示しないという設定になっているためです。

こちらを描画するにはGraphMachineクラスにおいてshow_auto_transitions引数をTrueにしてやる必要があります。


GraphMachineを用いた全状態遷移のグラフ出力例(全状態遷移を描画有効にして再定義) (transitions0.69/Linux/PyGraphviz版)

from transitions.extensions import GraphMachine

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

class Model(object):
pass

model = Model()
machine = GraphMachine(model=model, states=states, transitions=transitions, initial=states[0],
auto_transitions=True, ordered_transitions=False, title='',
show_auto_transitions=True) # 当行を追加
model.get_graph().draw('transitions.png', prog='dot', format='png') # ファイルで出力


上記のコードを再実行して出力された画像を表示してみます。

image.png

きちんと描画されました。

ユーザ定義外遷移において全状態遷移を設定しグラフ描画する際はお気を付けください。(本記事に限らずグラフ描画する際は通常全てshow_auto_transitions=Trueで描画しています)

なおユーザ定義外遷移においても順序遷移(ordered_transitions設定やadd_ordered_transitionsメソッド)は有効ですが、順序遷移についてはこういった表示設定がGraphMachineにあるわけではなく、きちんと描画されます。


まとめ

今回はtransitionsパッケージにおいてMachineクラスに設定できるユーザ定義外の遷移(全状態遷移/順序遷移)を紹介しました。

ユーザ定義外の遷移によりユーザが細かな遷移設定をしなくても状態遷移を実現できる事が理解できたかと思います。

一方でユーザが定義していないので、実際に何度かパターンを試したりグラフ描画してみないと遷移の予想がつきにくく使いどころが難しい印象はあるかと思います。

簡単な状態遷移やテスト時に有効な場合があったり、またデフォルトで有効になっている設定もあるので、こういった設定があることは頭の済みに入れておくと実装やデバッグが捗るかとは思います。

なおtransitionsパッケージに設定可能な遷移についてはHSM(Hierarchical State Machine : 階層型ステートマシン)という非常に複雑な遷移を実現できる定義もあるのですが今回はこれまで。

次回以降はコールバックについて詳細説明したいと思います。

以上、少しでもtransitionsパッケージの普及と皆様の理解の助けになればと思います。