初识状态机
通常意义上说的状态机特指有限状态机(Finite-State Machine,简称FSM),又称有限自动机(Finite-State Automaton,简称FSA )。按照Wikipedia的定义:
状态机是表示有限个状态及状态之间的转移和动作等行为的数学计算模型。
- 状态
状态存储着模型的历史信息,将历史信息的总和描述为一个状态。
- 转移
给定一个输入,当前状态要转换到哪个状态。
- 转移条件
转移条件也即事件、输入。一般情况下没有转移条件的可以虚拟出一个转移条件,这样子才能做到模型的一致性(简单来说就是通过虚拟的转移条件来简化模型,防止特殊处理的情况)。
- 动作
动作指的是在状态机迁移过程中在某个特定时刻执行的方程,也可以叫输出函数。一般包括:状态进入(只与当前状态有关)、状态转移(与当前状态和转移条件有关**),状态退出(只与当前状态有关)。**
这个状态机有点复杂,和我们平常使用的状态机好像不太一样,比如这个进入动作,退出动作,可能在实践中就比较少用,而且有的状态机好像就没有动作。
❕注 既然有有限状态机,相对应的也会有无限状态机,在真实场景中无限状态机的应用较少。因为无限状态,那么必然无法通过图灵机来模拟,也就无法实现。
状态机的分类
状态机在实践中有不同的划分方式,按照是否使用动作可以划分为接收器、变换器。变换器可以进一步划分为moore机、mealy机。按照状态转移是否明确(也就是对于给定的输入和状态是否仅有一种转移方式)可以划分为DFA(Deterministic Finite Automaton)、NFA(Nondeterministic Finite Automaton)。
接收器
上述的状态机可以简化成下面这个样子。在接收器中是没有动作的概念,只有状态、事件。但是状态会进一步区分为接受状态(用双圆圈表示)、非接受状态。
- 状态机处于接受状态:系统的输入被自动机所接受。
- 状态机处于非接受状态:系统的输入不被自动机所接受。
变换器
相比于接收器,变换器是有动作的状态机,针对使用动作的不同,可以进一步分为Moore机,Mealy机。
- Moore机
Moore机的特点是只使用了状态进入动作。这样子的好处就是简单,输出函数只和当前状态有关。因为如果有N个状态,M个事件,那么就有NM个转移的情况,也就是需要有NM个输出函数。如果只和状态有关,那么只需要N个输出函数。
- Mealy机
Mealy机的输出函数与当前状态和输入事件有关,也就是对应下图的转移函数,每个转移都会对应一个输出函数。
上面的Moore机和Mealy机是可以相互转换的,比如上面的Mealy机可以转化为如下的Moore机。可以看出转化为Moore机的时候状态变多了。
DFA
我们上面所说的所有状态机都是DFA**,每个状态在每个不同的输入事件下都有唯一确定的状态转移。**
NFA
NFA,即状态转移是不确定的。有以下两种情况
- 一个状态下对应着相同的输入可能有不同的状态转移。
- 在没有输入的情况下也可能发生转移。
NFA在编译原理里面有些非常广泛的应用,像正则匹配也是用的这类算法。NFA是可以转化为DFA。
关于NFA部分可以自己了解一下,这个比较复杂,这里不再介绍。以后有空再写一遍关于NFA的
状态机实现
按照我们前面定义的状态机,那么实现一个状态机有几个层面需要思考:
- 状态怎么定义?
- 事件怎么定义?
- 状态转移方程是什么样的?
- 输出函数定义
状态定义
状态机里面最核心的就是状态的定义,状态的定义直接影响到模型的好坏。好的状态应该能够满足以下几点
- 易于理解,不好理解意味着模型就会难以理解
- 足够简单,状态要尽量简化。在满足模型的情况下,状态应该尽可能少。
- 符合模型的定义。状态应该来源于模型定义。比如一个关于开关的模型,它应该是开关两种状态,而不是亮和灭两种状态。
事件定义
事件也就是外部输入,这个可以从模型的影响因素入手。也就是所建造的这个模型有哪些场景会导致模型发生变化。
状态转移
状态转移的重点在于穷举所有的状态和事件的组合。可以通过下面这种表格法帮助思考:
| | State1 | State2 | | --- | --- | --- | | Event1 | State2 | State2 | | Event2 | State1 | State1 |
代码实现
状态机的代码实现方式有三种方式:
- 分支模式
- 表模式
- 状态模式
接下来我们一起看看这些模式的代码实现是什么样,我们会以前面提到的开关门状态机为例子。状态机的图如下。
分支模式
这个是最容易想到的一种实现方式。简单来说就是通过各种if
判断当前状态以及触发的事件。
class DoorSM():
def __init__(self):
self.__state = "Opened"
def trigger(self, event):
if self.__state == "Opened":
if event == "DOWN":
print("pull down the door")
self.__state = "Closed"
if self.__state == "Closed":
if event == "UP":
print("pull up the door")
self.__state = "Opend"
dummy = DoorSM()
dummy.trigger("DOWN")
表模式
表模式也就是我们前面提到状态机转换表的一种代码转化,直接将状态转换图转化为代码的形式,下面代码中的transitions
就是状态转换表。比较适合用于实现mealy机,因为这里的输出函数其实没有和状态强绑定,整个编码模式其实是以状态转换为核心的。
class DoorSM():
def __init__(self):
self.__state = "Opened"
self.__transitions =\
{
"Opened":
{
"DOWN": ("Closed", self.actionA),
"UP": ("Opened", self.quiet)
},
"Closed":
{
"DOWN": ("Closed", self.quiet),
"UP": ("Opened", self.actionB)
}
}
def actionA(self):
print("pull down the door")
def actionB(self):
print("pull up the door")
def quiet(self):
print("Nothing happend")
def trigger(self, event):
(nextState, action) =\
self.__transitions[self.__state][event]
action()
print("State transit from", self.__state, "to", nextState)
self.__state = nextState
dummy = DoorSM()
dummy.trigger("DOWN")
dummy.trigger("UP")
从上面的代码可以看到如果我们的状态转移函数比较复杂的话会导致这个状态类实现变得越来越庞大。下面我们看另外一种实现
状态类实现
状态类实现的核心在于,将各个状态单独提取出来,原来的状态机只做一些公共的事情以及对外的接口呈现。这样做的好处在于某个状态需要修改的时候,只需要修改这个状态即可。比较适合Moore机
class DoorState():
def trigger(self):
raise
class DoorOpened(DoorState):
def __init__(self):
pass
def trigger(self, event):
nextState = self
if event == "DOWN":
print("pull down the door")
nextState = DoorClosed()
return nextState
class DoorClosed(DoorState):
def __init__(self):
pass
def trigger(self, event):
nextState = self
if event == "UP":
print("pull down the door")
nextState = DoorClosed()
return nextState
class DoorSM():
def __init__(self):
self.__state = DoorOpened()
def trigger(self, event):
self.__state = self.__state.trigger(event)
dummy = DoorSM()
dummy.trigger("DOWN")
dummy.trigger("UP")
各模式对比
优点 | 适用场景 | |
---|---|---|
分支模式 | 逻辑简单,易于实现 | 简单的状态,比如我们举的这个例子就比较合适,因为状态只有两个,状态转移动作也比较简单。 |
表模式 | 状态转换清晰易懂,容易保证无遗漏 | 状态,事件较多,但是状态转移动作比较简单。也就是复杂度在于状态转移方程。 |
状态模式 | 各个状态独立实现,互不影响 | 状态个数不多,但是每个状态下的动作比较多,也比较复杂,可能还有状态的进入退出动作。 |
💥这里的核心点在于: **复杂度在哪里就用对应的方式进行隔离。**如果是状态转移函数复杂,那么就通过表的形式,减少编码,防止出错。如果是单个状态复杂,那么就使用状态模式将各个状态隔离开。如果没有复杂的地方,那么使用分支模式,一眼就能看出所有的逻辑。