Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

transitionsはPythonで状態遷移を実現するためのパッケージですが、今回は状態遷移を実現するために「遷移」について紹介したいと思います。
この記事では状態遷移を実現するソフトウェア的な機構をステートマシンと呼んでいます。

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

Pythonで状態遷移を実装したり動作確認をしたい方に、Pythonの状態遷移パッケージ「transitions」の使い方を説明していきたいと思います。状態遷移そのものは組込みとか制御などでよく使われるものですが、それをPythonで実現したい場合にこのパッケージが有用かと思います。
今回は状態遷移において重要な「遷移」(transitions)に関して詳細説明します。「遷移編1」として、ユーザ定義の遷移や定義可能な遷移の種類、遷移に関するコールバック、ガード判定および遷移に設定できる項目や操作のサマリーを紹介します。
その他、transitionsの概要やインストール方法、グラフ表示機能の設定については準備編の記事を参照頂けたらと思います。
※今回の記事では公式チュートリアルにならい遷移をtransitionsと呼んでますが、パッケージ名のtransitionsとややこしいので、当パッケージそのものを示す場合はtransitionsパッケージと明示することにします。

状態遷移とイベント

状態遷移における「遷移」とは、ある状態にイベントが発生した場合などで「現在の状態」から「別の状態」に変わったり、「現在の状態」から抜けて再度「同じ状態」に戻ってくることを言います。後者は自己遷移などと言ったりもします。また状態からは抜け出ないでイベントが発生した時のアクション(コールバック)のみを行うものを内部遷移などという場合もあります。当記事における遷移の種類と名称をUML(Unified Modeling Language)のステートマシン図に照らし合わせると以下のようになります。
image.png

状態を遷移させるのは基本的にイベントになりますが、システムに入力されるイベントは様々なものが定義されると思います。すべてのイベントに対して遷移する場合もありますが、通常は「とある状態」の時に「とあるイベント」が起こった際に状態が遷移することになります。この「ある状態において遷移を伴うイベント」のことを「トリガー」や「トリガーイベント」と言ったりします。

遷移(transitions)の定義と動作例

transitionsパッケージでは状態遷移を実現するために各種イベントと遷移の定義をしますが、基本的には遷移元、遷移の引き金になるトリガーイベント、そして遷移先を定義する形になります。
※本記事において「遷移」や「遷移の定義/設定」と言った場合は主にMachineクラスやGraphMachine等のMachine派生クラスの引数transitionsに設定するオブジェクトに関する事を示します。

まずは状態AとBがあって、fromAtoBというトリガーイベントで遷移する場合、遷移(transitions)の定義は以下のようになります。

最も基本的な遷移の定義例
from transitions import Machine

states = ['A', 'B']
transitions = {'trigger':'fromAtoB', 'source':'A', 'dest':'B'}  #遷移の定義

class Model(object):
    pass

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

上記は状態、遷移、ステートマシン全てを定義していますが、実際に遷移を定義している部分は以下になります。

transitions = {'trigger':'fromAtoB', 'source':'A', 'dest':'B'}

遷移の定義は基本的に辞書形式で行いますが、遷移元は'source'キー、遷移先は'dest'キーで指定し、遷移の起点となるトリガーイベントは'trigger'キーで指定します。
この辞書形式で定義した遷移をMachineクラスの引数transitionsに指定しmachineオブジェクトを生成することで、model引数に指定されたmodelオブジェクトに対し「定義された遷移」を持つステートマシンが実装されます。(以降この遷移設定を行う辞書のことをtransitions辞書と呼びます)
今回の状態遷移は2状態1遷移を定義していますが、こちらを図示すると以下の通りになります。
image.png

冒頭の例における状態遷移は状態「A」である時に「fromAtoB」イベントが起こった際、状態「B」に遷移するという内容になります。
また、実際にコードに組み込む際は遷移だけでなく、状態(states)の定義も忘れないようご注意ください。

ここで上記コード例で定義した状態遷移を動かしてみましょう。
イベントの起こし方としては、ステートマシンを付与されたオブジェクト(例ではmodel)+「イベント名()」で実施できます。
また、同オブジェクト+「trigger(イベント名)」を使ってもステートマシンにイベントを与えることが可能です。

最も基本的な遷移の実行例
>>> model.state         # 初期状態の確認
'A'
>>> model.fromAtoB()    # トリガーイベント。model.trigger('fromAtoB')でも可
True
>>> model.state         # 遷移後の状態の確認
'B'

初期状態「A」から、トリガーイベント「fromAtoB」によって状態「B」に遷移していることが確認できます。
GraphMachineで図示すると以下の通りになります。
(※Machineクラスでは画像出力できません。GraphMachineを使った画像出力については準備編をご確認ください)
image.png

複数の遷移定義

複数の遷移を同時に設定することも可能です。
冒頭の例では状態「A」から状態「B」に関する遷移しか定義していないので、状態「B」から状態「A」に戻る事ができません。
以下の例では状態「B」から状態「A」に戻る遷移とトリガーイベント「fromBtoA」を追加した例になります。
複数の遷移をtransitionsに指定する場合は辞書形式で定義した各遷移設定をリストにする必要があることにご注意下さい。

状態BからAへの遷移追加時の例
from transitions import Machine

states = ['A', 'B']
transitions = [
    {'trigger':'fromAtoB', 'source':'A', 'dest':'B'},
    {'trigger':'fromBtoA', 'source':'B', 'dest':'A'},
]

class Model(object):
    pass

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

GraphMachineでグラフ化するとfromBtoAというイベントとBからAへの遷移が追加されていることが確認できると思います。
image.png

遷移の種類

さて、遷移元のsourceと遷移先のdestを定義し、その起点イベントとなるtriggerにより状態遷移が実現できることが理解できたと思いますが、transitionsパッケージは1状態のみの遷移だけでなく複数状態(全状態)からの遷移や設定、自己遷移/内部遷移についても定義できるのでそちらについても紹介していきたいと思います。
以下、全てtransitions辞書に関する設定内容についての詳細になります。

自己遷移/内部遷移

まずは自己遷移の定義から。自己遷移は一旦自身の状態から抜けてまた自分自身の状態に入ってくるという意味合いがありますので、sourceとdestに同じ状態を定義すれば可能です。
一方で自己状態を示す文字'='をdestに指定することで、同様に自己遷移を実現できます。後述する一括設定の応用例でもあるとおり、'='を用いると状態によらず自己状態を指定できるので自己遷移を定義する際は'='をdestに指定することをオススメします。

transitions = {'trigger':'self', 'source':'A', 'dest':'='}  #'source':'A', 'dest':'A'でもよい

自己遷移をGraphMachineで描画してみると以下の通り遷移が自分自身に返っていることが確認できると思います。
状態Aの時にselfイベントが発生するとプログラム的には状態Aを一旦抜け状態Aに遷移しますがmodel.stateでイベント前後を確認しても状態が変わったように見えない点に注意ください。
もちろん自己遷移でのトリガーイベント名も'self'だけでなく自由に付けることが可能です。
image.png

次に内部遷移についてですが、内部遷移は今の状態から抜けて再度同じ状態に入る訳ではなく、定義されたトリガーイベントにより各種アクション(コールバック)を実施する際に使われます。
ですので、内部的には遷移のような動きはするものの同じ状態に「入る/出る」という事象は起こらず、トリガーイベントを受けて各種遷移に関するコールバックが起こることになります。(自己遷移でも遷移に関するコールバックは実施される)

内部遷移を実現するには遷移先(dest)にNoneを指定するだけで可能です。
以下の例では特に遷移に関するコールバックが定義されているわけではないので挙動は上記自己遷移と同様になります。

transitions = {'trigger':'self', 'source':'A', 'dest':None} 

内部遷移をGraphMachineで描画してみると以下の通り自己遷移と同様に遷移が自分自身に返っていますが、[internal]の文字が付与されていることが確認できると思います
image.png

transitionsパッケージにおける自己遷移と内部遷移の違いとしては、自己遷移はトリガーイベントにより遷移が確定すればon_enterやon_exitといった状態(state)に関するコールバックも含め実施されることになります。一方で内部遷移は状態に入る/出るという事象が起こらないので状態(state)に関するコールバックon_enter/on_exitが起こらないという事が異なります。もちろん状態に関するコールバックon_enter/on_exitが定義されていなければ自己遷移と特に大きな差はありませんが、非常に重要な点なので自己遷移や内部遷移でコールバックを設定する際はご注意ください。

複数状態からの遷移(複数状態への一括設定)

複数状態(全状態)に対する遷移定義については、遷移元(source)に複数の状態をリストで指定するか、全状態指定であるアスタリスク(*)を用いることで実現可能です。
複数の状態に対し一括して一つのイベントで同じ状態に遷移する場合などに便利で、例えば以下のように全状態や複数の通常状態から終了状態へ遷移するような定義の仕方が考えられます。

transitions = {'trigger':'to_end', 'source':['A', 'B', 'C'] 'dest':'End'}

image.png

遷移元sourceに全状態指定'*'を指定した場合、全ての状態において指定されたtriggerイベントが発生すると遷移先(dest)への遷移が発生します。
なお複数状態と全状態指定'*'の違いとしては、全状態指定は遷移先の状態(dest)も含め全ての状態が含まれるので自己遷移が発生します。
以下遷移元(source)に全状態指定'*'した場合の例になりますが、ここでは状態Endに自己遷移(End→Endの遷移)が追加されている点に注意下さい。
全状態指定'*'を行う場合は予想外の遷移が発生していないか注意してご使用下さい。

transitions = {'trigger':'to_end', 'source':'*' 'dest':'End'}

image.png

その他、複数状態への一括設定の応用として、公式チュートリアルでも紹介されているとおり、一括して内部/自己遷移が可能です。
例えば以下のコードであれば状態A, B, Cに対し同じイベント(self)で内部遷移を実現できます。

transitions = {'trigger':'self', 'source':['A', 'B', 'C'] 'dest':'='}

image.png

遷移先/遷移元(source/dest)に関するの設定一覧

上記を含め設定可能な遷移の種類をまとめると以下の通りになります。
これ以外にHSM(Hierarchical State Machine/階層型ステートマシン)もありますがそちらについては別の機会に紹介します。

遷移元(source) 遷移先(dest) 説明
状態'A' 状態'B' 状態Aから状態Bへの遷移
状態'A' '=' 状態Aに自己遷移を設定
状態'A' None 状態Aに内部遷移を設定
複数状態(リスト) 状態'A' 複数の状態(状態A除く)から状態Aへの遷移
複数状態(リスト) '=' 複数の状態に自己遷移を設定
複数状態(リスト) None 複数の状態に内部遷移を設定
'*' 状態'A' 全状態から状態Aへの遷移(自己遷移も加わる)
'*' '=' 全状態に自己遷移を設定
'*' None 全状態に内部遷移を設定

注意点として、遷移元であるsourceには元々複数状態をlistで指定できる一方で、遷移先であるdestは複数状態を定義できず遷移先は一意である必要があります(destにおいてNoneや'='は自分自身と解釈される)。
ですからsourceにおいて全状態指定を示すアスタリスク'*'は指定できても、destに全状態指定'*'を定義できません。destにアスタリスク'*'を指定した場合「'*'」という状態名への遷移という形になり全状態への遷移という形にはなりませんのでご注意を。
同様にsourceには遷移上「自己/内部」を示す意味でNoneや'='は指定できず、sourceに'='を指定した場合は「=」という状態名からの遷移と解釈されてしまいます。

遷移に関するコールバック

状態遷移を実装する利点としては設計の見通しをよくしたり保守性を上げたりと、様々な点が挙げられますが遷移に伴いコールバックを実施することも利点の一つに挙げられると思います。
今までtransitionsパッケージに関する記事を書いてきましたが、これまでの内容だけではイベントを起こして状態が変わったりグラフを書くだけのお絵かきツールの一つのように思えるかもしれませんが、遷移に伴いコールバックが実施させると非常に強力な制御ツールとなると思います。

transitionsパッケージでは様々なコールバック指定がありますが、ここでは遷移に絡む(transitions辞書や後述するadd_transition(s)メソッドで設定する)コールバックを紹介します。
コールバックは遷移だけでなく状態に対してやステートマシンにも設定できますが、優先順位などが絡むので詳細についてはコールバック編で記載します。
遷移に関するコールバックのみを実現する場合は当記事のみで実装いただければ充分かと思います。

遷移に関するコールバックとは

遷移に関するコールバックとは、その名の通り「とある状態においてトリガーイベントが与えられた際に実施されるコールバック」であり、通常ステートマシンが付与されたmodelオブジェクト内のクラス内メソッドを呼び出す形になります。
従って基本的には遷移前(source)にトリガーイベント(trigger)が発生した際に実施されるコールバックを指定するのですが、その実施タイミングによって以下のようにいくつか種類があります。

  • prepare :遷移前に実施されるコールバック。後述するガード判定で遷移が中断される場合も実施される。(ガード判定前に実施される)
  • before  :トリガーイベントが発生し遷移が確定した際、遷移前の状態(souce)で実施されるコールバック
  • after   :トリガーイベントが発生し遷移が確定した際、遷移後の状態(dest)で実施されるコールバック

ある状態において遷移(内部/自己遷移含む)が確定した場合、遷移に関するコールバックはprepare → before → afterという順で実施されるようになりますが、必ずしも全てのコールバックを定義する必要はありません。また状態(State)に関するコールバックおよびMachineに関するコールバックが定義されている場合は状態遷移に伴いそれらコールバックも実施されることがあり、実施順については注意が必要です(コールバック編で紹介予定)

以下、遷移に関するコールバックの定義例になります。
各コールバックメソッドにおいては実行時に自身のメソッド名を印字する処理が入っているだけです。

遷移に関するコールバックの定義例
import sys
from transitions import Machine

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

# 遷移とコールバックの定義。コールバックメソッドは文字列でメソッド名を定義
transitions = [
    {'trigger':'fromAtoB', 'source':'A', 'dest':'B', 
     'prepare':'action_prepare', 'before':'action_before', 'after':'action_after'}  
]

class Model(object):
    def __init__(self, filename=None):
        self.output = filename

    # prepareに関するコールバック
    def action_prepare(self):
        print('do "{}" on state ({})'.format(sys._getframe().f_code.co_name, self.state))

    # beforeに関するコールバック
    def action_before(self):
        print('do "{}" on state ({})'.format(sys._getframe().f_code.co_name, self.state))

    # afterに関するコールバック
    def action_after(self):
        print('do "{}" on state ({})'.format(sys._getframe().f_code.co_name, self.state))

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

こちらを動作させてみると以下の通り、それぞれprepareとbeforeは遷移前のsource(状態A)で実施されており、afterは遷移後のdest(状態B)で実施されている事が確認できるかと思います。

遷移に関するコールバックの実行例
>>> model.state
'A'
>>> model.fromAtoB()    # トリガーイベントを発生させ、設定したコールバックが実施されるか確認
do "action_prepare" on state (A)
do "action_before" on state (A)
do "action_after" on state (B)
True
>>> model.state
'B'

まとめるとトリガーイベントが発生した際のコールバックと発生箇所は以下の通りとなります。
image.png
なお、prepareとbeforeの使い分けですが、beforeは遷移が確定していないと実施されないのに対し、prepareは遷移が確定していなくても実施されます。
これは後述するガード判定において「トリガーイベントが発生したが遷移がガードされた」場合にprepareは実施されますがbefore(もちろんafterも)は実施されません。

従って、遷移時のみにコールバックを実施する場合はafterやbeforeにコールバックを定義すると良いでしょう。またコールバック内でステートマシンの状態(state)を使う処理を行う場合はbeforeとafterで状態が異なるので、状態遷移前後の状態を意識してコールバックの実施タイミング設計をされると良いかと思います。

遷移時のガード判定

状態遷移におけるガードとは「トリガーイベントが発生した際、特定条件において遷移を行わない」というもので、要は遷移に設定された「とある条件」が成立(もしくは不成立)時のみに遷移を行うというものです。
補足:UMLのステートマシン図におけるガード条件と同じようなものだとイメージしていただければと思います。
image.png
また通常UMLにおいてガード条件は「ガード条件成立時に遷移する」というものですが、transitionsパッケージの条件成立時(conditions)以外に不成立時(unless)に遷移する定義があるため、当記事ではガード"判定"と呼称を変更しています。

ガード判定(conditions/unless)の定義と例

transitionsパッケージにおいてガード判定はconditionsもしくはunlessで行います。
conditions/unlessの判定処理は共にコールバックとして設定しますが、両者の違いとして、conditionsはTrue時に遷移を許可し、unlessはFalse時に遷移を許可するという定義になります。
従ってconditionsを設定する場合は「コールバックがTrueを返す際に遷移する」、unlessを設定する場合は「コールバックがFalseを返す際に遷移する」という動作を実現できます。

以下、ガード判定(conditions)の例を見ていきましょう。
まずconditionsによる遷移成立時(ガード条件成立時遷移)の定義と動作確認についてです。
conditionsはトリガーイベントが発生した際に実施されますが、遷移前(source)で先にconditions内のコールバックが実施され、conditionsからTrueが返った際に遷移が行われます。

遷移成立時(conditions)の定義例
import sys
from transitions import Machine

states = ['A', 'B']   # 状態の定義
transitions = [
    {'trigger':'fromAtoB', 'source':'A', 'dest':'B', 'conditions':'action_conditions'}
]

class Model(object):
    # ガード判定用コールバック(conditionsやunlessはbool型の戻り値が必要)
    def action_conditions(self):
        print('do "{}" on state ({})'.format(sys._getframe().f_code.co_name, self.state))
        return True             # conditionsではTrueを返すと遷移を許可する(unlessではFalse時に遷移を許可)

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

上記コード定義後、以下のコードにより動作させてみると、conditionsに設定したクラス内メソッドがコールバックされ実施されている事が分かります。
内部的な一連の動きとしては、トリガーイベント発生 → condisionsのコールバック実施 → コールバックよりTrueが返る → destへ遷移という流れになります。

遷移成立時(conditions)の挙動
>>> model.state
'A'
>>> model.fromAtoB()    # トリガーイベントを起こしてみる
do "action_conditions" on state (A)
True
>>> model.state         # 状態はBになり状態遷移できている。
'B'

次にconditionsによる遷移阻止時(ガード条件不成立時の遷移阻止)の定義と動作確認についてです。
遷移成立時の例との違いは一つだけで、ガード判定用コールバックの戻り値をFalseにしているだけです。
condisionsを用いたガード判定においては、conditionsからFalseが返った際に遷移が阻止されます。

遷移阻止時(conditions)の定義例
import sys
from transitions import Machine

states = ['A', 'B']   # 状態の定義
transitions = [
    {'trigger':'fromAtoB', 'source':'A', 'dest':'B', 'conditions':'action_conditions'}
]

class Model(object):
    # ガード判定用コールバック(conditionsやunlessはbool型の戻り値が必要)
    def action_conditions(self):
        print('do "{}" on state ({})'.format(sys._getframe().f_code.co_name, self.state))
        return False            # conditionsではFalseを返すと遷移を阻止する(unlessではTrue時に遷移を阻止)

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

実際に動作させてみます。
上記コード定義後、以下のコードにより動作させてみると、遷移を促すためのトリガーイベントが発生しているのにもかかわらずイベント発生前後で状態が変わっていません。
これはガード判定(conditions)により遷移阻止(ガード条件不成立)となった為です。
内部的な一連の動きとしては、トリガーイベント発生 → condisionsのコールバック実施 → コールバックよりFalseが返る → 遷移阻止(状態はsourceのまま)という流れになります。

なお、ガード判定を定義した際にトリガーイベントを入力して遷移できたかどうかはイベント前後の状態(model.state)を確認すれば判定できますが、それ以外にトリガーイベントを起こした際の戻り値(以下ではmodel.fromAtoB()の戻り値)を確認すれば判定できます(True:状態遷移した、False:状態遷移しなかった)。

遷移阻止時(conditions)の挙動
>>> model.state
'A'
>>> model.fromAtoB()    # トリガーイベントを起こしてみる
do "action_conditions" on state (A)
False
>>> model.state         # 状態はAのままで状態遷移できていない。
'A'

unlessについてはtransitions辞書の定義において'unless'キーを用いてコールバックを設定すれば同様に動作しますので例は割愛します。
ただし前述の通り遷移する条件としてはconditionsと論理が反転しますのでご注意ください。

ガード判定とコールバック(prepare/before/after)の関係

ガード判定(conditions/unless)に設定されるものもコールバックですが、実施順や遷移阻止された場合に実施されるされないがコールバックごとに異なります。
基本的にガード判定により遷移が成立した場合、prepare/before/after全て実施されることになります。
一方で、ガード判定により遷移が阻止された場合はbefore/afterは実施されないことになります。以下、実際に例で確認してみます。

遷移に関するコールバックと遷移阻止のガード判定の定義
import sys
from transitions import Machine

states = ['A', 'B']   # 状態の定義
transitions = [
    {'trigger':'fromAtoB', 'source':'A', 'dest':'B', 'conditions':'action_conditions',
     'prepare':'action_prepare', 'before':'action_before', 'after':'action_after'}
]

class Model(object):
    # ガード判定用コールバック
    def action_conditions(self):
        print('do "{}" on state ({})'.format(sys._getframe().f_code.co_name, self.state))
        return False            # 遷移を阻止させるためFalse

    # prepareに関するコールバック
    def action_prepare(self):
        print('do "{}" on state ({})'.format(sys._getframe().f_code.co_name, self.state))

    # beforeに関するコールバック
    def action_before(self):
        print('do "{}" on state ({})'.format(sys._getframe().f_code.co_name, self.state))

    # afterに関するコールバック
    def action_after(self):
        print('do "{}" on state ({})'.format(sys._getframe().f_code.co_name, self.state))

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

上記コードを定義後、以下の通りトリガーイベントを発生させると遷移は阻止されますが、その際に実施されているコールバックをご確認下さい。
補足:上記コードのうちaction_conditionsの戻り値をTrueに変更すれば遷移成立時の挙動を確認できると思います。

遷移阻止時における呼び出されるコールバックの挙動確認
>>> model.state
'A'
>>> model.fromAtoB()    # トリガーイベントを起こし、どのコールバックが起こるか確認する
do "action_prepare" on state (A)
do "action_conditions" on state (A)
False
>>> model.state
'A'

上記挙動確認より、condisionsで遷移が阻止(戻り値:False)されているため、before/afterに設定されたコールバックが実施されていないことが確認できると思います。
また同時にprepareはcondisionsより前に実施されており、コールバックの項目でも言及しましたが、ガード判定の結果に関わらずprepareのコールバックが実施されている事が確認できると思います。
上記挙動を図示すると以下のようになります。
image.png

prepareではガード判定前に実施されることから、「トリガーイベントが起こった際にガード判定に必要な前処理をprepareで行い、実際遷移させるかどうかの判定処理をガード判定で行う」という思想が見えるかと思います。もちろんprepareにはガード判定の前処理以外の処理を行っても構いませんが、ガード判定を行う場合はガード判定コールバック内であれこれ処理を実施するよりかはprepareを設定しprepareコールバック内で前処理を実施した方がガード判定時の処理の見通しが良くなる場合が多いです。

以下、遷移に関するコールバック(ガード判定含む)の一覧と、ガード判定による遷移阻止時の実行可否について記します。

実施順 コールバック 実施される状態 遷移阻止時も実施される
1 prepare 遷移元 (souce) YES
2 conditions 遷移元 (souce) YES ※1
3 unless 遷移元 (souce) YES ※1, ※2
4 before 遷移元 (souce) NO
5 after 遷移先 (dest ) NO

※1:conditionsとunlessが同時設定されていた場合、両者の遷移条件が成立していないと遷移しません。
※2:conditionsとunlessが同時設定されていた場合、conditionsで遷移阻止された場合はunlessも実施されません。

なお、上記の表はあくまでも「遷移に関するコールバック」のみに関する優先順位になります。
繰り返しになりますが、状態(State)やMachineに関するコールバックが加わるとそれらのコールバックが差し込まれる場合もあるので、遷移以外のコールバックも実装する場合はコールバック編(作成中)をご確認ください。

遷移に関する設定

遷移に関する設定としては遷移そのもの(コールバックやガード判定含む)についてがありますが、それ以外に遷移の挙動に関する設定をMachineクラスで設定できますので、そちらについても記載いたします。

遷移定義(transitions辞書)に対する設定

上記までの遷移(transitions)に関する内容をまとめると以下の通りになります。
以下表の内容はMachineのtransitions引数に設定するtransitions辞書として設定する内容ですが、後述するadd_transition(s)関数なども同様の項目を使って設定することができます。
引数/keyについてですが、複数項目を同時に設定できる項目については文字列のリストで一括設定可能です。

項目 key 初期値 説明
トリガーイベント名 trigger str -(なし) 遷移の起点となるトリガーイベント名
遷移元状態名 source str/List -(なし) 上記トリガーイベントを受け入れる状態
遷移先状態名 dest str -(なし) 上記sourceでtriggerが起こった際に遷移する状態
ガード判定(True) conditions str/List None 指定されたコールバックがTrue時に遷移を許可
ガード判定(False) unless str/List None 指定されたコールバックがFalse時に遷移を許可
遷移準備コールバック prepare str/List None triggerが発生した際に実行されるコールバック
遷移前コールバック before str/List None 状態が遷移する前に呼び出されるコールバック
遷移後コールバック after str/List None 状態が遷移した後に呼び出されるコールバック

Machineに対する設定

上記transitions辞書などはMachineクラス(GraphMachine等の派生クラス含む)に設定する内容ですが、Machineクラスの引数に設定する遷移に関する内容としては以下も挙げられます。

項目 引数 初期値 説明
無効イベント無視 ignore_invalid_triggers bool false True設定ではsourceに定義されていないイベントが
起こった際、MachineError例外を発生させない

無効イベント無視について

Machineクラスの初期設定においては、sourceに定義されていないイベントが発生した場合はMachineErrorが発生するようになっています。
例えば冒頭の「最も基本的な遷移の定義例」の状態BにおいてfromAtoBメソッドを実施した場合です。

無効イベントの実施とMachineError例外の発生
>>> model.state         # 遷移前の状態の確認
'B'
>>> model.fromAtoB()    # 無効イベントの実施(source:状態BにおいてfromAtoBイベントに関する遷移が定義されていない場合)
Traceback (most recent call last):

transitions.core.MachineError: "Can't trigger event fromAtoB from state B!"

これを防ぐにはMachineError例外をtry/exceptionで例外処理するか、Machineに対し無効イベント無視(ignore_invalid_triggers引数)をTrueに設定してやる必要があります。

無効イベント無視を設定する場合の定義例
from transitions import Machine

states = ['A', 'B']
transitions = {'trigger':'fromAtoB', 'source':'A', 'dest':'B'}  # 遷移の定義

class Model(object):
    pass

model = Model()
machine = Machine(model=model,
                  states=states, transitions=transitions, initial=states[1],    # 初期状態を'B'に設定
                  auto_transitions=False, ordered_transitions=False,
                  ignore_invalid_triggers=True)

上記の定義を以下の確認コードで動作させてみるとMachineErrorが発生せず、遷移も行われていない事が確認できます。
また、トリガーイベントへの戻り値もFalseになっており、状態Bの定義外イベント発生において遷移が実施されていないことが確認できると思います。

無効イベント無視=True時の挙動
>>> model.state         # 初期状態の確認
'B'
>>> model.fromAtoB()    # 無効イベントの実施(source:状態BにおいてfromAtoBイベントに関する遷移が定義されていない)
False
>>> model.state         # イベント発生後の状態の確認
'B'

無効イベント無視(ignore_invalid_triggers)はMachineクラスだけでなく各状態に対し個別に設定も可能です(参考:状態編/状態に関する設定
Machineクラスに無効イベント無視設定を行った場合、全ての状態において定義外のイベントが発生した際は無視されるようになりますが、状態(State)に対し個別設定した場合は無効イベント無視設定した状態(source)でのみMachineErrorを発生させないという形になりますので注意が必要です。

無効イベント無視は例外により処理を中断させない設定となりますが、そもそも定義外のイベントが起こるのはステートマシン設計者の想定外のイベントの発生の場合が殆どだと思いますので、何でもかんでも無視するのではなく、状態/遷移と発生しうるイベントの設計や確認を状態遷移表などを用いてしっかりやった方が良いとは思います。ですのでデバッグ時等は無効イベント無視を無効(False)にしておき、例外処理として発生状況を確認できるようにしておいた方が無難かと思います。一応、設定としては存在するので紹介まで。

遷移に関する操作

遷移に関する操作としては、遷移の確認、追加、削除が挙げられます。
状態に関する操作とは異なり、遷移においては削除に関する操作があります。なお、状態の総合的な管理はMachineクラスで作成されたmachineオブジェクトが行っているため、machineオブジェクトで操作することが主となりますが、ステートマシンが付与されるmodelオブジェクトでも一部状態に関する操作が行えます。

メソッド/変数 対象オブジェクト 戻り値 引数    説明
イベント名 model bool 通常なし イベント名()で指定イベントをmodelに引き起こす
遷移できればTrueが返る
trigger model bool trigger 引数triggerに設定されているイベントをmodelに引き起こす。
戻り値は上記に同じ
events machine dict machineに設定されているイベント情報を返す。
メソッドではなく変数
get_triggers machine list state 引数stateに設定されているトリガーイベントを返す
get_transitions machine list trigger, souce, dest 引数triggerに指定したイベントの遷移情報を返す
add_transition machine transitions辞書の設定 設定項目を引数指定しmachineに遷移を追加する
add_transitions machine transitions辞書 transitions辞書形式でmachineに遷移を追加する
remove_transition machine trigger, souce, dest 引数triggerに指定したイベントの遷移を削除する

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

以下、状態に関する各操作についての詳細と例を示します。
イベントを引き起こすメソッド(イベント名、triggerメソッド)は何度か出ているので割愛します。
遷移に関する操作による変化を確認するため、以下のコード/状態遷移を定義し、それら状態に対して各種操作を実施してみます。

遷移の操作に関する操作を実施するための状態定義
from transitions import Machine

states = ['A', 'B', 'C']
transitions = [
    {'trigger':'fromAtoB', 'source':'A', 'dest':'B'},
    {'trigger':'fromAtoC', 'source':'A', 'dest':'C'},
    {'trigger':'fromBtoA', 'source':'B', 'dest':'A'} 
]

class Model(object):
    pass

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

上記をGraphMachineで生成すると以下の状態遷移図が得られます。
以降の説明において遷移の定義に変化のある操作についてはGraphMachineの結果も同時に載せます。
image.png

イベント情報の取得

遷移関連の確認項目としてはevents変数、get_triggersメソッドを用いてmachineに設定されているイベント情報を確認することができます。

① get_triggersメソッドを使った方法

こちらは引数の状態(state)を元にトリガーイベントを取得します。
ここでは設定するstateは遷移元(source)になり、その状態に結びついたトリガーイベントが取得される事になります。

>>> machine.get_triggers('A')
['fromAtoB', 'fromAtoC']

最も簡単なイベントの取得方法ですが、引数に状態を指定する必要があります。
引数無しや定義していない状態についても指定できますが、その場合は空リストが返ってきます。

② machine.eventsを使った方法

こちらもmachineに現在設定されているイベントの一覧を辞書(dict)形式で取得できます。
取得された辞書は{"イベント名", transitions.Event型}形式で取得できます。

>>> machine.events
{'fromAtoB': <Event('fromAtoB')@2448921859296>, 'fromAtoC': <Event('fromAtoC')@2448921860528>, 'fromBtoA': <Event('fromBtoA')@2448921861032>}

イベント情報のみをリストで取るならば以下のコードで十分かと思います。]

>>> [event_name for event_name in machine.events.keys()]
['fromAtoB', 'fromAtoC', 'fromBtoA']

 
補足:machine.eventsから遷移情報(Transition)を取得する場合
eventsアクセサから各イベントとイベントが持つ遷移を取得することは可能ですが、階層が深いので結構大変です。
以下にeventsアクセサで取得した際の階層構造を示します。
image.png

eventsアクセサでは各イベントの設定(dict)→Event→transitionsという形で遷移設定まで追えますが、さらにこの遷移設定(transitions)は以下のように2段階のlist構造になっています。
これは1つのイベントに複数の階層/遷移が定義されている場合を考慮しているからだとは思いますが、遷移設定(Transitions)まで階層を深くしています。
まとめるとEvent->transitions->transitionと取得し、このtransitionクラスにアクセスすることで各イベントに結びついた遷移情報を取得できます。
image.png

細かいことを書きましたが、ともあれeventsアクセサから遷移情報を取得する場合は以下のコードで可能です。
以下ではMachineに設定された全イベントの取得とそれらイベントに結びつく遷移、および遷移情報の表示を行っています。

machine.eventsより遷移情報を取得する例
events = machine.events
for event in events.values():
    print("event:", event.name)
    t_All = event.transitions.values()
    for t_list in t_All:
        for transition in t_list:
            transition_val = transition.__dict__
            for member in transition_val.keys():
                print('{} : {}'.format(member, transition_val[member]))
        print()
    print()
実行結果
event: fromAtoB
source : A
dest : B
prepare : []
before : []
after : []
conditions : []
以下略

細かく書きましたが、正直なところ遷移情報だけを取得する場合は後述のget_transitionsメソッドを使った方が楽です。

遷移情報の取得

イベントではなく遷移情報(Transition)を取得する場合、get_transitionsメソッドを使います。
こちらは引数の状態(state)を元に遷移情報(Transition)を取得します。
※Transitionsクラスのメンバーは「② machine.events」の補足項目をご参照下さい。

ここで引数に指定するstateは遷移元状態(source)になり、その状態に結びついたトリガーイベントが取得される事になります。
引数はtrigger, source, destがありますが、それらは必須指定ではなく指定されない場合は全てのtrigger, source, destが対象になります。

まずは引数無しで実施した場合。こちらはMachineに定義された遷移一覧が取得できます。

>>> machine.get_transitions()
[<Transition('A', 'B')@1693057862344>, <Transition('A', 'C')@1693057996616>, <Transition('B', 'A')@1693057996728>]

次にトリガーイベントのみを指定した場合。1つのトリガーで複数の遷移がある場合はそれらも表示されます。

>>> machine.get_transitions(trigger='fromAtoB')
[<Transition('A', 'B')@1693057862344>]

次は遷移元(source)のみを指定した場合。こちらはその状態から遷移する遷移情報が取得できます。

>>> machine.get_transitions(source='A')
[<Transition('A', 'B')@1693057862344>, <Transition('A', 'C')@1693057996616>]

次は遷移先(dest)のみを指定した場合。こちらはその状態へ遷移する遷移情報が取得できます。

>>> machine.get_transitions(dest='A')
[<Transition('B', 'A')@1693057996728>]

補足:get_transitionsとトリガーイベント
get_transitionsの注意点としては、このメソッドからだけではトリガーイベント情報を取得できないという点です。
従って、「Machineに定義づけられたイベントに結びつく遷移情報の一覧が欲しい」場合はget_transitionsメソッドのみでは不可能です。
ですので、その場合は「② machine.events」項目の補足にある方法で取得するか、以下のコードの通りmachine.eventsとget_transitionsの合わせ技にすると良いでしょう

machine.eventsとget_transitionsメソッドより遷移情報を取得する例
events = [event_name for event_name in machine.events.keys()]
for event in events:
    print("event:", event)
    transitions = machine.get_transitions(event)
    for t in transitions:
        for member in t.__dict__.keys():
            print('{} : {}'.format(member, transition_val[member]))
        print() 
    print()

実行結果は「② machine.events」項目の「machine.eventsより遷移情報を取得する例」と同じになるので割愛します。

遷移の追加

遷移の追加としてはadd_transitionメソッドとadd_transitionsメソッドを用いて実現できます。
1文字しか違わないメソッド名ですが、地味に引数での指定方法が異なるのでご注意ください。
使い分けとしてはtransitions辞書を用いたり複数辞書を一度に登録する場合はadd_transitionsメソッドを使うという点でしょうか。

遷移の追加について共通する最大限注意すべきものとしては、遷移元(source)に全状態指定'*'を設定した定義を行っている場合です。
Machine定義時や遷移追加設定に遷移元(source)='*'とある場合、遷移が追加されるのはあくまでも「その時点で存在する状態」のみに対し全状態遷移が設定されます。
従って、全状態指定'*'により遷移を追加した後に、さらにadd_stateやadd_transition(s)を実行されて追加された状態は含まれない点にご注意下さい。

① add_transitionメソッドを使った方法

こちらは遷移を追加しますが、遷移情報を引数の形で指定するものになります。
設定可能な引数はtransitions辞書に対する設定と同じになりますが、key名を引数名と置き換えて設定下さい。
ひとまず状態Cにおいて状態Aへの遷移をfromCtoAイベントで実施する例を見てみましょう。

add_transitionメソッドを用いた遷移の追加
>>> machine.add_transition(trigger='fromCtoA', source='C', dest='A')

image.png

② add_transitionsメソッドを使った方法

こちらも遷移を追加するメソッドになります。
前述のadd_transitionメソッドと異なるのは引数にtransitions辞書をとる点です。またMachineクラスにtransitionsを設定する際と同様に、transitions辞書リストとして複数の遷移情報を設定しておけば1回の実行で複数の遷移を追加できるというものです。
当章の最初に定義したコードに対し、状態C → 状態A、状態C → 状態A、状態B → 状態Cの遷移を追加してみます。

add_transitionsメソッドを用いた遷移の追加
>>> transitions = [
        {'trigger':'fromCtoA', 'source':'C', 'dest':'A'},
        {'trigger':'fromCtoB', 'source':'C', 'dest':'B'},
        {'trigger':'fromBtoC', 'source':'B', 'dest':'C'},
    ]
    machine.add_transitions(transitions)

image.png

遷移の削除

遷移の削除はremove_transitionメソッドで行う事ができます。
こちらはtrigger引数のみは省略できませんが、sourceとdestは省略可能です。その場合triggerに結びついた全ての遷移が削除されます。
特定の状態における遷移を削除する場合はsourceやdestを指定すると良いでしょう。

遷移の削除例
>>> print('before remove:', [event_name for event_name in machine.events.keys()])
    machine.remove_transition('fromAtoB')
    print('after remove:', [event_name for event_name in machine.events.keys()])
before remove: ['fromAtoB', 'fromAtoC', 'fromBtoA']
after remove: ['fromAtoC', 'fromBtoA']

※ちなみに当削除例についてtransitions0.69/GraphMachine(Graphvis/windows版)で描画しようとしたところ、削除された遷移が残ってしまうと言う現象に遭遇しました。
こちらはGraphMachine上でGraphvizの更新ミスというバグと考えられますが、Machineクラスでは上記のとおりきちんと動作するのを確認していますので、使用する場合はMachineオブジェクトでご使用下さい。

まとめ

今回はtransitionsパッケージにおいてMachineクラスに設定できるユーザ定義の遷移(transitions)を紹介しました。
ユーザ定義の遷移にはトリガーイベント(trigger)、遷移前状態(source)、遷移後状態(dest)があり、それらについて様々な遷移を設定でき、さらに遷移に伴うコールバック、ガード判定ができることを示しました。また当記事でイベントや遷移の取得、追加、削除を行えることが確認できたかと思います。
ここまで読まれた方は当パッケージを用いて状態、遷移、コールバックを設定し、状態遷移を実現するステートマシンを実現できると思います。
一方でこれまで何回か(暗黙に)使っていたユーザ定義外の遷移や、遷移以外のコールバックについては詳細な紹介してませんので、次回以降はユーザ定義外の遷移やコールバックについて詳細説明したいと思います。

noca
その辺にいるしがない組込み屋です。 CやC#、pythonなどを使ったりします。たまに機械学習関係もいじったりしてます。 Qiita初心者です。何か有りましたらご助言いただけたら幸いです。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away