Python state-machine design

jldupont picture jldupont · Jan 20, 2010 · Viewed 50.1k times · Source

Related to this Stack Overflow question (C state-machine design), could you Stack Overflow folks share your Python state-machine design techniques with me (and the community)?

At the moment, I am going for an engine based on the following:

class TrackInfoHandler(object):
    def __init__(self):
        self._state="begin"
        self._acc=""

    ## ================================== Event callbacks

    def startElement(self, name, attrs):
        self._dispatch(("startElement", name, attrs))

    def characters(self, ch):
        self._acc+=ch

    def endElement(self, name):
        self._dispatch(("endElement", self._acc))
        self._acc=""

    ## ===================================
    def _missingState(self, _event):
        raise HandlerException("missing state(%s)" % self._state)

    def _dispatch(self, event):
        methodName="st_"+self._state
        getattr(self, methodName, self._missingState)(event)

    ## =================================== State related callbacks

But I am sure there are tons of ways of going at it while leveraging Python's dynamic nature (e.g. dynamic dispatching).

I am after design techniques for the "engine" that receives the "events" and "dispatches" against those based on the "state" of the machine.

Answer

S.Lott picture S.Lott · Jan 20, 2010

I don't really get the question. The State Design pattern is pretty clear. See the Design Patterns book.

class SuperState( object ):
    def someStatefulMethod( self ):
        raise NotImplementedError()
    def transitionRule( self, input ):
        raise NotImplementedError()

class SomeState( SuperState ):
    def someStatefulMethod( self ):
        actually do something()
    def transitionRule( self, input ):
        return NextState()

That's pretty common boilerplate, used in Java, C++, Python (and I'm sure other languages, also).

If your state transition rules happen to be trivial, there are some optimizations to push the transition rule itself into the superclass.

Note that we need to have forward references, so we refer to classes by name, and use eval to translate a class name to an actual class. The alternative is to make the transition rules instance variables instead of class variables and then create the instances after all the classes are defined.

class State( object ):
    def transitionRule( self, input ):
        return eval(self.map[input])()

class S1( State ): 
    map = { "input": "S2", "other": "S3" }
    pass # Overrides to state-specific methods

class S2( State ):
    map = { "foo": "S1", "bar": "S2" }

class S3( State ):
    map = { "quux": "S1" }

In some cases, your event isn't as simple as testing objects for equality, so a more general transition rule is to use a proper list of function-object pairs.

class State( object ):
    def transitionRule( self, input ):
        next_states = [ s for f,s in self.map if f(input)  ]
        assert len(next_states) >= 1, "faulty transition rule"
        return eval(next_states[0])()

class S1( State ):
    map = [ (lambda x: x == "input", "S2"), (lambda x: x == "other", "S3" ) ]

class S2( State ):
    map = [ (lambda x: "bar" <= x <= "foo", "S3"), (lambda x: True, "S1") ]

Since the rules are evaluated sequentially, this allows a "default" rule.