Help with coroutine-based state machines?

Alan Kennedy alanmk at hotmail.com
Fri May 30 06:47:12 EDT 2003


Terry Reedy wrote:

> I very much like the idea of factoring out the unneeded redundancy of
> representing states by name strings instead of by the state objects
> themselves.

Thanks for all your great points Terry.

Indeed, factoring them out made this one case simpler for me to understand. But
as the good Dr. Mertz has pointed out in another message, there are many real
world situations where having a jump-table (maybe "dispatch-table" is a better
term?) indexed by instances of your input data tokens is the best approach in
terms of clarity and efficiency.

> The generator functions, as instance methods, are class attributes in
> the *class* dictionary.  They should typically be called exactly once
> for each instance of the machine, with that instance as the argument.
> The obvious place to do this is in .__init__().  So I would not call
> this hackery.

Thanks for pointing out my misunderstanding. I only realised my mistake when I
went to rewrite the code to incorporate some of the suggested improvements.

When I actually went to iterate through the instance dictionary looking for
gen-funcs, I found there were no funcs there at all! They were of course on the
class dictionary. So, combining this with 

1. Your other great tip of storing the gen-it.next methods (not the gen-its
themselves) in the instance dictionary,
2. Carel Fellinger's nicer setattr/getattr call instead of the eval(),
3. Using a naming convention to differentiate state gen-funcs from other
gen-funcs and funcs

The __init__ method now looks like

def __init__(self):
    for name in self.__class__.__dict__.keys():
        if type(self.__class__.__dict__[name]) is types.FunctionType \
            and name[:2] == 's_':
            setattr(self, name, getattr(self, name)().next)

> The resulting gen-it objects, being instance specific, belong and get
> put in the *instance* dict.  Again, this is not hackery.  Because you
> used the same names, they overshadow rather than replace their
> namesakes, which continue to sit, unchanged,  in the class dict.  You
> can still access FSM.idle as exactly that, even
> from the same instance.

This is now completely clear to me, many thanks. This means that if any of the
gen-its get destroyed, it could be recreated. So it would probably be better to
factor out the gen-it creation into a ".reset()" function, which could be called
between invocations of the state machine, as well as from the __init__.

> There is also a little more redundancy to squeeze out.  You only need
> the gen-its to access their .next methods and the lookup only needs to
> be done once, in .__init__, rather than with each state switch.  So
> you could move '.next' from .dispatch to .__init__ to get
> 
>     self.__dict__[name] = eval ('self.g_%s()' % name).next
> and
>     next = newstate()
> (instead of newstate.next()).  In other words, represent states by the
> corresponding .next methods instead of the gen-its that have those
> methods.  Since the .next functions do not have parameters, not even
> self (since self is bound within by the gen-func call), they also
> belong in the instance dict.

Thanks for that last remaining contraction, which more cleanly describes the
actuality of dispatching, in the coroutine sense. This makes the dispatch loop
even cleaner, especially when combined with Carel Fellinger's tip of checking an
instance flag for the exit condition on the loop, instead of looping infinitely:

def dispatch(self, startstate):
    state = startstate
    while state != self.s_exit:
        state = state()

Ahhh, sublime! (Still seeking that "Zen of Python" smilie, if anyone has one?)

The updated version of the code then becomes

#----------------------------------------------
# This code is also available at
# http://xhaus.com/alan/python/FSMgen2py.html
#

import types

class FSM:

    def __init__(self, startnum, incrnum, stopnum):
        for name in self.__class__.__dict__.keys():
            if type(self.__class__.__dict__[name]) is types.FunctionType\
                    and name[:2] == 's_':
            	setattr(self, name, getattr(self, name)().next)
                #self.__dict__[name] = eval ('self.%s()' % name).next
        self.startnum = startnum
        self.incrnum = incrnum
        self.stopnum = stopnum
        self.counter = 0
        self.s_exit = 'exitsentinel'

    def s_idle(self):
        while 1:
            #    We could wait for some condition here.
            print "Idle state: %d" % self.counter
            yield self.s_start

    def s_start(self):
        while 1:
            self.counter = self.startnum
            print "Start state: %d" % self.counter
            yield self.s_increment

    def s_increment(self):
        while 1:
            self.counter = self.counter + self.incrnum
            print "Increment state: %d" % self.counter
            yield self.s_checkfinished

    def s_checkfinished(self):
        while 1:
            if self.counter >= self.stopnum:
                yield self.s_finished
            else:
                yield self.s_increment

    def s_finished(self):
        while 1:
            print "Finished state: %d" % self.counter
            yield self.s_exit

    def dispatch(self, startstate):
        state = startstate
        while state != self.s_exit:
            state = state()

if __name__ == "__main__":
    m = FSM(1,5,50)
    m.dispatch(m.s_idle)
    m.dispatch(m.s_idle)
#----------------------------------------------

Thanks to all who took the time to help me understand.

kind regards,

alan kennedy
-----------------------------------------------------
check http headers here: http://xhaus.com/headers
email alan:              http://xhaus.com/mailto/alan




More information about the Python-list mailing list