由于项目需要,使用到了Python的状态机库 Transitions ,但GitHub上是英文文档,使用过后觉得还不错,遂手工翻译一下,加深理解,水平有限,如有翻译不当之处请Email (kevin920902@gmail.com) 我。
Transitions
Python 实现的轻量级、面向对象状态机,兼容Python 2.7+ 和 Python 3.0+。
安装方法
方法一
1 | pip install transitions |
方法二
直接在 GitHub 上clone仓库到本地,然后
1 | python setup.py install |
快速入门
俗话说,接触一个新的工具库,100页的API文档、一千字的描述文档或更多的解释说明都不如一个简单的Demo,虽然这个说法我们无法验证真伪,但下面的例子会让你快速了解 Transitions
的基本用法
1 | from transitions import Machine |
现在我们已经建立了一个 NarcolepticSuperhero
状态机,来看看具体怎么使用吧
1 | >>> batman = NarcolepticSuperhero("Batman") |
详细文档
基本初始化方式
使状态机正常运行非常简单。 假设你有一个对象 lump
( Matter
类的一个实例) ,并且你想要管理它的状态:
1 | class Matter(object): |
我们将 lump
绑定到状态机上来初始化一个最小状态机
1 | from transitions import Machine |
之所以称之为最小状态机,因为这种状态机在技术上是可操作的,状态机初试状态为 solid
,但因为我们没有增加任何的转移动作(transitions:这应该是个动词,我在这翻译为转移动作), 所以它实际上什么也没做。
下面我们来定义更多的状态和转移动作
1 | # The states |
注意:我们为 Matter
实例 lump
绑定了 evaporate()
ionize()
等方法,每个方法触发相应的状态转换,你不必在任何地方明确定义这些方法,每个转换的名称通过绑定到模型上来传递给状态机初始化函数(lump
)。evaporate()
ionize()
另等方法都是静态来触发状态转移,如果我们想实现动态转移,可以使用trigger
方法。
状态集
毫无疑问,状态机的核心是状态集,上述介绍中我们通过传递给 Machine
初始化函数字符串列表来定义有效的模型状态,但是在 Machine
内部,状态以 State
对象的形式存储。
我们可以通过多种方式初始化和跟新状态,比如:
- 传递一个给定状态名的字符串给
Machine
初始化函数; - 直接初始化每个新的
State
对象; - 传递具有初始化参数的字典。
以下代码片段说明了实现相同目标的几种方法
1 | # Create a list of 3 states to pass to the Machine |
回调方法
我们可以给一个状态添加一系列的”进入“和”退出“的回调函数,还可以在初始化期间指定回调,或者稍后再添加回调。
为方便起见,每当新的状态被添加到 Machine
中时,方法 on_enter_«state name»
和 on_exit_«state name»
都是在 Machine
上(而不是 Model
上)上动态创建的,并且允许我们动态地添加新的进入和退出回调函数。
1 | # Our old Matter class, now with a couple of new methods we |
注意:首次初始化状态机时, on_enter_«state name»
回调函数是不会触发的。例如,我们定义了一个回调函数 on_enter_A()
,并给状态机初始化状态为 initial='A'
,on_enter_A()
会在下次进入状态A的时候出发,首次并不会触发。如果我们想在初始化的时候就触发 on_enter_A()
方法,可以创建一个虚拟的初始状态,然后调用 to_A()
方法,而不是 __init__
方法。
除了在初始化状态机传递回调或动态添加回调,为增加代码的清晰度,还可以在模型类本身中定义回调。例如
1 | class Matter(object): |
现在,任何时候 lump
转移到状态A,类Matter
中的 on_enter_A()
方法总是会触发。
检查状态
我们可以随时通过以下方式检查模型的当前状态
- 检查
.state
属性; - 调用
is_«state name»()
方法。
如果要检索当前状态的实际状态对象,可以通过 Machine
实例的 get_state()
方法来实现
1 | lump.state |
状态转换
上面的一些例子已经说明了状态转移的使用,但是在这里我们将更详细地探讨它们。
与状态一样,每个状态转换在内部用 Transitions
对象来表示,我们可以传递一个字典或字典列表来快速初始化状态转换集合。
如上面我们已经写过的
1 | transitions = [ |
使用字典定义转换有利于代码的清晰,但显得比较笨重。比较简单的方法是使用列表定义转换,只需确保每个列表中的元素与 Transition
初始化中的位置参数的顺序相同(比如 trigger
, source
, destination
等)
我们可以这样定义转换
1 | transitions = [ |
另外,我们也可以在状态机初始化之后添加转换
1 | machine = Machine(model=lump, states=states, initial='solid') |
trigger
参数定义了绑定到模型上的触发方法的名称,当回调这个方法的时候,状态机会执行相应的状态转换。
1 | >>> lump.melt() |
当我们调用非法的转换时,Machine
会抛出异常
1 | >>> lump.to_gas() |
这种做法通常是可取的,因为它有助于提醒代码中的问题。但是在某些情况下,我们希望忽略这些非法转换,这时可以设置 ignore_invalid_triggers=True
(可以全部忽略,也可忽略特定一条)
1 | >>> # Globally suppress invalid trigger exceptions |
如果在做状态转换之前,希望知道在当前状态下,哪些是合法的转换,可以使用 get_triggers()
方法
1 | m.get_triggers('solid') |
自动转换所有状态
除了显式添加的任何转换之外,每当将状态添加到 Machine
实例时,都会自动创建一个 to_«state»()
方法,该方法无论状态机当前处于哪种状态,都会转换到目标状态。
1 | lump.to_liquid() |
我们可以在状态机初始化的时候设置 auto_transitions=False
禁用这个功能。
多状态转换
给定的触发器可以附加到多个转换,其中一些可以在相同的状态下潜在地开始或结束。比如
1 | machine.add_transition('transmogrify', ['solid', 'liquid', 'gas'], 'plasma') |
在这种情况下,如果当前状态是 plasma
调用 transmogrify()
,会转换到 solid
状态,然后继续转换到 plasma
状态。注意:只有第一个匹配的转换将执行,因此,上述最后一行中定义的转换将不会做任何事情。
我们还可以通过使用 *
通配符来引发触发器从所有状态转换到特定状态
1 | machine.add_transition('to_liquid', '*', 'liquid') |
一个反身触发(触发器具有与源和目标相同的状态)可以使用“=”来表示目的状态,比如通过 touch
触发,solid
的目标状态为自身。如果我们希望同一个反身触发器应用到多个状态中,使用“=”是比较方便的。
1 | machine.add_transition('touch', ['liquid', 'gas', 'plasma'], '=', after='change_shape') |
通过上面的语句, 'liquid', 'gas', 'plasma'
三个状态通过 touch
触发器,可以到达的目的状态分别是 'liquid', 'gas', 'plasma'
。
有序转换
比如我们现在有这样一个需求,状态的转换是遵循自定义的序列的,如给定状态 ['A', 'B', 'C']
,我们可能需要这样的状态转换 A → B, B → C, C → A
(没有其他的非法转换)。为了实现这个功能,Transitions
在 Machine
类中提供了 add_ordered_transitions()
方法
1 | states = ['A', 'B', 'C'] |
排队转换
Transitions中的默认行为是立即处理事件。也就是说,调用绑定在 after
上的回调函数之前, on_enter
方法中的事件会预先处理。
1 | def go_to_C(): |
上述状态机的执行顺序为:
1 | prepare -> before -> on_enter_B -> on_enter_C -> after. |
如果启用排队处理,则在触发下一个转换之前,转换将完成:
1 | machine = Machine(states=states, queued=True) |
执行顺序为:
1 | prepare -> before -> on_enter_B -> queue(to_C) -> after -> on_enter_C. |
注意:当使用排队处理事件时,触发器的调用始终返回True,所以在排队执行过程中,无法确定一个包含排队回调函数的转换最终是否能成功,即使是在处理一个事件的时候。
1 | machine.add_transition('jump', 'A', 'C', conditions='will_fail') |
条件转换
在某些情况下,我们可能需要在某些条件成立的时候去执行特定的转换,这个时候,conditions
就派上用场了。
1 | # Our Matter class, now with a bunch of methods that return booleans. |
在上面的示例中,如果模型初始化的状态为 solid
,当条件 is_flammable
成立时,调用 heat()
会转换到 gas
状态。同样,如果条件 is_really_hot
成立,调用 heat()
会转换到 liquid
状态。
我们还可能在某些条件不成立的条件下,转换到另一状态,Transitions
为我们提供了 unless
方法。
1 | machine.add_transition('heat', 'solid', 'gas', unless=['is_flammable', 'is_really_hot']) |
这时,如果 is_flammable(), is_really_hot()
返回的都是 False
,模型调用 heat()
会从 solid
转换到 gas
状态。
注意:条件检查方法会被动接收传递给触发函数的参数或数据对象。比如:
1 | lump.heat(temp=74) |
这样会将 temp=74
的可选参数传递给 is_flammable()
方法(以 EventData
实例的方式),参数的传递我们在后面会介绍。
回调方法
我们可以将回调方法绑定到状态集和转换集上,每一个转换都包含 before
和 after
属性,这也是我们比较常用的,这两个属性包含了一系列在状态转换前后可以调用的方法。
1 | class Matter(object): |
在状态转换一开始,在其他转换开始执行之前,我们也可以使用 prepare
回调函数:
1 | class Matter(object): |
注意:除非当前状态是定义在转换集上有效的状态,否则,prepare
回调是不会起作用的。
如果在每一个转换执行之前或之后都需要进行一些默认操作,我们可以在状态机初始化的时候显示使用 before_state_change
和 after_state_change
属性。
1 | class Matter(object): |
还有一些可以独立使用的关键字:
prepare_event
:通过prepare_event
关键字传递给状态机的回调方法,在处理可能发生的状态转换(包括私有的prepare
回调方法)之前,仅执行一次;finalize_event
:通过finalize_event
关键字传递给状态机的回调方法,不管转换是否成功,都会被执行;send_event
:如果在状态转换过程中出现了错误,这个错误会绑定到event_data
上,我们可以使用send_event=True
来检索错误 。
1 | from transitions import Machine |
执行命令
以下是转换集上回调方法可以执行的命令:
回调 | 状态 | 备注 |
---|---|---|
machine.prepare_event |
源状态 | 在私有转换执行之前仅执行一次 |
transition.prepare |
源状态 | 转换一开始就执行 |
transition.conditions |
源状态 | 可使转换失败或停止 |
transition.unless |
源状态 | 可使转换失败或停止 |
machine.before_state_change |
源状态 | 声明在模型上的默认回调方法 |
transition.before |
源状态 | |
state.on_exit |
源状态 | 声明在源状态上的回调方法 |
<STATE CHANGE> |
||
state.on_enter |
目的状态 | 声明在目的状态上的回调方法 |
transition.after |
目的状态 | |
machine.after_state_change |
目的状态 | 声明在模型上的默认回调方法 |
machine.finalize_event |
源状态/目的状态 | 即使出现错误或异常,回调方法也会执行 |
传递数据
在大多数情况下,我们需要传递给注册在状态机中的回调方法一些参数,来反应当前模型的状态和进行一些必要计算。Transitions
提供了两种方法。
第一种方法(默认方法):
我们可以直接传递参数给触发器方法。
1 | class Matter(object): |
我们通过这种方法传递任何参数给触发器方法。
但是这种方式有一种局限性:状态转换触发的每一个回调方法都必须处理所有的参数。如果我们的不同回调方法需要不同的参数,这样就会导致一些不必要的麻烦。
为了解决这个问题,Transitions
提供了另一种方法,上文中我们提到过,参数可以以EventData
实例的方式传递。在Machine
初始化的时候设置 send_event=True
,这样,所有传递给非触发器的参数都以EventData
实例的方式传递给回调方法,为了方便我们随时访问与事件相关的源状态、模型、转换、触发等参数,EventData
实例会维护这些参数的内部引用。
1 | class Matter(object): |
其他初始化方式
在以上所有的示例中,我们将新的 Machine
实例绑定到一个单独的模型上(Matter
类的实例 lump
),虽然这种方式可以使我们的类和回调方法比较整洁,但是我们必须能跟踪到状态机上调用的方法,并且知道模型调用的这些方法绑定了哪个状态机(比如: lump.on_enter_StateA()
vs machine.add_transition()
)
庆幸的是,Transitions
为我们提供了两种不同的初始化方式。
第一种方式,我们可以创建一个独立的状态机,完全不需要其他模型,在初始化的时候忽略模型参数。
1 | machine = Machine(states=states, transitions=transitions, initial='solid') |
如果我们使用这种方式初始化状态机,我们就可以直接绑定所有的触发事件和回调方法到 Machine
实例上。
这种方法有利于在一个地方整合所有的状态机功能,但如果考虑到状态逻辑应该包含在模型本身中,而不是单独出来,这种方式恐怕也不是最好的。
一种替代的办法是让 Machine
要绑定的模型继承 Machine
类,只要重写 Machine
的 __init__()
方法即可:
1 | class Matter(Machine): |
现在我们已经整合所有的状态机功能到 lump
模型中,这比将功能独立出来让人感觉更舒服。
如果我们绑定多个模型到 Machine
中,状态机也是可以处理的。如果想要添加模型和 Machine
实例本身,我们可以在初始化的时候使用 self
关键字( Machine(model=['self', model1, ...])
),也可以创建一个单独的状态机,然后通过 machine.add_model
动态注册模型到状态机中,如果模型不再使用的情况下,应该调用 machine.remove_model
来回收模型。
1 | class Matter(): |
如果在状态机初始化的时候没有提供初始状态,在添加模型的时候就必须提供这个初始状态。
1 | machine = Machine(states=states, transitions=transitions, add_self=False) |
日志
Transitions
提供一些基本的日志功能,使用 Python
标准的 logging
模块将一些如状态转换、转换触发、条件检查等信息作为 INFO
级别的信息记录下来,我们可以在脚本中轻松地将日志记录配置为标准输出:
1 | # Set up logging |
存储/恢复状态机实例
如果想要存储或加载状态机,必须使用Python3.3
或较早版本的 dill
。
1 | import dill as pickle # only required for Python 3.3 and earlier |
以上介绍了 Transitions
的基本使用方法,手工翻译难免有些晦涩难懂之处,敬请谅解。除了这些基本用法之外,Transitions
还提供了一些扩展方法,比如图表可视化机器的当前状态、用于嵌套和重用的分层状态机、并行执行的线程安全锁等,这里暂时就不介绍了,先欠着吧,等以后深入使用 Transitions
库的时候再去学习。
Transitions
英文完整版介绍请移步 Transitions