状态机从理论上说是几乎和计算机和编程相关每件事基础从实用角度来看状态机还有助于解决许多常见问题(特别适用于 Python 员)本文中David Mertz 讨论了何时以及如何使用 Python 编码状态机实际例子
什么是 Python?
Python 是由 Guido van Rossum 开发免费高级解释型语言其语法简单易懂而其面向对象语义功能强大(但又灵活)Python 可以广泛使用并具有高度可移植性
什么是状态机?
有关状态机个极度确切描述是它是个有向图形由组节点和组相应转移组成状态机通过响应系列事件而“运行”每个事件都在属于“当前”节点转移控制范围内其中范围是节点个子集返回“下个”(也许是同个)节点这些节点中至少有个必须是终态当到达终态状态机停止
但个抽象数学描述(就像我刚给出)并不能真正介绍说明在什么情况下使用状态机可以解决实际编程问题另种策略就是将状态机定义成种强制性编程语言其中节点也是源码行从实用角度看这个定义尽管精确但它和第种描述样都是纸上谈兵、毫不实用(对于介绍说明型、型或基于约束语言例如Haskell、Scheme 或 Prolog不定会发生这种情况)
让我们尝试使用更适合身边实际任务例子来进行讨论逻辑上每个规则表达式都等价于个状态机而每个规则表达式语法分析器都实现这个状态机实际上大多数员编写状态机时并没有真正考虑到这点
在以下这个例子中我们将研究状态机真正探索性定义通常我们有些区别思路方法来响应组有限数量事件某些情况下响应只取决于事件本身但在其它情况下适当操作取决于以前事件
本文中讨论状态机是高级机器其目是演示类问题编程解决方案如果有必要按响应事件行为类别来讨论编程问题那么您解决方案很可能是显式状态机
=atitle>文本处理状态机
最可能显式状态机个编程问题涉及到处理文本文件处理文本文件通常包括读取信息单元(通常叫做或行)然后对刚读取单元执行适当操作某些情况下这个处理是“无状态”(即每个这样单元都包含了足够信息可以正确确定要执行什么操作)在其它情况下即使文本文件不是完全无状态数据也只有有限上下文(例如操作取决于不比行号更多信息)但是在其它常见文本处理问题中输入文件是极具“状态”每块数据含义取决于它前面串(也许是它后面串)报告、大型机数据输入、可读文本、编程源文件和其它种类文本文件都是有状态个简单例子是可能出现在 Python 源文件中行代码:
=section>
myObject = SomeClass(this, that, other)
这行表示如果恰好有以下几行围绕着这行则有部分内容区别:
=section>
"""How to use SomeClass:
myObject = SomeClass(this, that, other)
"""
我们应知道我们处于“块引用” 状态 以确定这行代码是部分注释而不是 Python 操作
=atitle>何时不使用状态机
当开始为任何有状态文本文件编写处理器任务时问问自己您希望在文件中找到什么类型输入项每种类型输入项都是种状态候选项这些类型共有几种如果数字很大或者不确定则状态机也许不是正确解决思路方法(在这种情况下某些数据库解决方案可能更适合)
还请考虑您是否需要使用状态机许多情况下最好从更简单思路方法入手也许会发现即使文本文件是有状态也有种简单思路方法可以分块读取它(其中每块是种类型输入值)实际上在单状态块中仅当文本类型的间转移需要基于内容计算时才有必要实现状态机
下面这个简单例子介绍说明了需要使用状态机情况请考虑用于将列数字划分成几块两个规则在第个规则中列表中零表示块的间间断第 2个规则中当个块中元素总和超过 100 时会发生块的间间断由于它使用累加器变量来确定是否达到了阈值您不能“马上”看到子列表边界因此第 2个规则也许更适合于类似于状态机机制
稍微有些状态、但由 不 太适合用状态机处理文本文件例子是 风格 .ini 文件这种文件包括节头、注释和许多赋值例如:
=section>
; the colorscheme and userlevel
[colorscheme]
background=red
foreground=blue
title=green
[userlevel]
login=2
title=1
我们例子没有实际含义但它表明了 .ini 格式些有趣特性
- 就某种意义而言每行类型由它第个确定(可能是分号、左花括号或字母)
- 从另种角度看这种格式是“有状态”关键字 "title" 大概表示如果它出现在每节中那么就有独立内容
处理 .INI 文件分块 Python 代码
=section>
=boldcode>
import
txt = open(
'hypothetical.ini').read
sects = .split(txt,
'[')
=boldcode>
for
sect
=boldcode>
in
sects:
# do something with sect, like get its name
# (the stuff up to ']') and read its assignments
或者如果愿意可以使用单个
current_section
变量来确定位置: 处理 .INI 文件计算 Python 代码
=section>
=boldcode>
for
line
=boldcode>
in
open(
'hypothetical.ini').readlines:
=boldcode>
line[0]
'[':
current_section = line(1:-2)
=boldcode>
el
line[0]
';':
=boldcode>
pass
# ignore comments
=boldcode> : apply_value(current_section, line)
=atitle>何时使用状态机
现在我们已经决定了如果文本文件“太简单”就不使用状态机让我们再研究 需要使用状态机情况本专栏中 最近篇文章 讨论了实用 Txt2Html它将“智能 ASCII”(包括本文)转换成 HTML让我们扼要重述
“智能 ASCII”是种文本格式它使用些间隔约定来区分文本块类型如头、常规文本、引语和代码样本虽然读者或作者能容易地通过查看分析这些文本块类型的间转移但却没有简单思路方法可以让计算机将“智能 ASCII”文件分割成组成它文本块不像 .ini 文件举例文本块类型可以任何顺序出现在任何情况下都没有单定界符来分隔块(空行 通常 分隔文本块但代码样本中空行却不定结束代码样本并且文本块不需要用空行来分隔)由于需要以区别方式重新格式化每个文本块以生成正确 HTML 输出状态机似乎就是自然解决方案
Txt2Html 阅读器般功能如下:
- 在状态中启动
- 读取行输入
- 根据输入和当前状态转移到新状态或按适合当前状态方式处理该行
Python 中个简单状态机输入循环
=section>
=boldcode>
global
state, blocks, bl_num, block
#-- Initialize the globals
state = "HEADER"
blocks = [""]
bl_num = 0
block = 1
=boldcode>
for
line
=boldcode>
in
fhin.readlines:
=boldcode>
state
"HEADER":
# blank line means block of header
=boldcode>
blankln.match(line): block = 1
=boldcode>
el
textln.match(line): startText(line)
=boldcode>
el
codeln.match(line): startCode(line)
=boldcode>
:
=boldcode>
block: startHead(line)
=boldcode>
: blocks[bl_num] = blocks[bl_num] + line
=boldcode>
el
state
"TEXT":
# blank line means block of text
=boldcode>
blankln.match(line): block = 1
=boldcode>
el
headln.match(line): startHead(line)
=boldcode>
el
codeln.match(line): startCode(line)
=boldcode>
:
=boldcode>
block: startText(line)
=boldcode>
: blocks[bl_num] = blocks[bl_num] + line
=boldcode>
el
state
"CODE":
# blank line does not change state
=boldcode>
blankln.match(line): blocks[bl_num] = blocks[bl_num] + line
=boldcode>
el
headln.match(line): startHead(line)
=boldcode>
el
textln.match(line): startText(line)
=boldcode>
: blocks[bl_num] = blocks[bl_num] + line
=boldcode>
:
=boldcode>
raise
ValueError,
"unexpected input block state: "+state
可以用 Txt2Html 从中取出该代码源文件(请参阅 参考资料 )请注意:变量
state
声明为 global
在(如 startText
)中更改它值转移条件如 textln.match
是规则表达式模式但它们可能也是定制实际上以后会在中执行格式化状态机只将文本文件分析成 blocks
列表中带标签块=atitle>抽象状态机类
在表单和中使用 Python 实现抽象状态机很容易这使状态机模型比前个例子中简单条件块显得更突出(初看其中条件和其它条件没有什么区别)而且以下类及其关联处理在隔离状态中操作方面完成得很好许多情况下这改善了封装和可读性
文件:statemachine.py
=section>
=boldcode>
from
=boldcode>
import
upper
=boldcode>
StateMachine
:
=boldcode>
def
__init__
(self):
self.handlers = {}
self.startState = None
self.endStates =
=boldcode>
def
add_state
(self, name, handler, end_state=0):
name = upper(name)
self.handlers[name] = handler
=boldcode>
end_state:
self.endStates.append(name)
=boldcode>
def
_start
(self, name):
self.startState = upper(name)
=boldcode>
def
run
(self, cargo):
=boldcode>
try
:
handler = self.handlers[self.startState]
=boldcode>
except
:
=boldcode>
raise
"InitializationError",
"must call ._start before .run"
=boldcode>
=boldcode>
not
self.endStates:
=boldcode>
raise
"InitializationError",
"at least _disibledevent=>
while
1:
(State, cargo) = handler(cargo)
=boldcode>
upper(State)
=boldcode>
in
self.endStates:
=boldcode>
=boldcode>
:
handler = self.handlers[upper(State)]
StateMachine
类实际上正是抽象状态机所需要使用 Python 传递对象是如此简单和其它语言中相似类比较这个类所需使用行数非常少 要真正 使用
StateMachine
类需要为每个要使用状态创建些处理处理必须符合模式它循环处理事件直到要转移到另个状态此时处理应该将个字节组(它包括新状态名称以及新状态处理需要任何 cargo)传递回去 在
StateMachine
类中将 cargo
用作变量做法将封装状态处理所需数据(该状态处理不必它 cargo
变量)状态处理使用 cargo
来传递下个处理所需内容于是新处理可以接管前个处理遗留工作 cargo
通常包括文件句柄它允许下个处理可以在前个处理停止后读取更多数据 cargo
还可能是数据库连接、复杂类例子或带有几个项列表 现在让我们研究测试样本在本例中(在以下代码举例中概述)cargo 只是不断将反馈传送给迭代个数字只要
val
处于某个范围内则 val
下个值永远只是 math_func(val)
旦返回了超出范围值那么该值将传送到另个处理或者状态机在了个什么也不做终态处理后就退出举例介绍说明了件事: 事件不必是输入事件它也可以是计算事件(这种情况很少)状态处理相互的间区别只是在输出它们处理事件时使用区别标记该比较简单没必要使用状态机但它很好地介绍说明了概念代码也许比解释更易于理解! 文件:statemachine_test.py
=section>
=boldcode>
from
statemachine
=boldcode>
import
StateMachine
=boldcode>
def
_disibledevent=>(val):
=boldcode>
pr
"ONES State: ",
=boldcode>
while
1:
=boldcode>
val <= 0
=boldcode>
or
val >= 30:
State =
"Out_of_Range" ;
=boldcode>
el
20 <= val < 30:
State =
"TWENTIES";
=boldcode>
el
10 <= val < 20:
State =
"TENS";
=boldcode>
:
=boldcode>
pr
" @ %2.1f+" % val,
val = math_func(val)
=boldcode>
pr
" >>"
=boldcode>
(State, val)
=boldcode>
def
tens_counter
(val):
=boldcode>
pr
"TENS State: ",
=boldcode>
while
1:
=boldcode>
val <= 0
=boldcode>
or
val >= 30:
State =
"Out_of_Range";
=boldcode>
el
1 <= val < 10:
State =
"ONES";
=boldcode>
el
20 <= val < 30:
State =
"TWENTIES";
=boldcode>
:
=boldcode>
pr
" #%2.1f+" % val,
val = math_func(val)
=boldcode>
pr
" >>"
=boldcode>
(State, val)
=boldcode>
def
twenties_counter
(val):
=boldcode>
pr
"TWENTIES State:",
=boldcode>
while
1:
=boldcode>
val <= 0
=boldcode>
or
val >= 30:
State =
"Out_of_Range";
=boldcode>
el
1 <= val < 10:
State =
"ONES";
=boldcode>
el
10 <= val < 20:
State =
"TENS";
=boldcode>
:
=boldcode>
pr
" *%2.1f+" % val,
val = math_func(val)
=boldcode>
pr
" >>"
=boldcode>
(State, val)
=boldcode>
def
math_func
(n):
=boldcode>
from
math
=boldcode>
import
sin
=boldcode>
abs(sin(n))*31
=boldcode>
__name__
"____":
m = StateMachine
m.add_state(
"ONES", _disibledevent=>
最新评论