4
\$\begingroup\$

I'm trying to find the best - read: readable, maintainable, robust, threadsafe, usable - solution for a State Machine in python. For this I've been looking at the State Design Pattern. However I want a pythonic and lean solution (saying not implementing functions in subclasses that are not needed).

Long story short, here's what I came up with. What do you think?

#!/bin/env python
# -*- utf-8 -*-
"""Boiler Plate code for implementing a state machine according to
state design pattern
"""
class State(object):
 """Base class for a state in a statemachine
 No statemachine class is needed. State transitions have to be
 defined in self.transitions.
 The object is callable and by calling it input is handled and
 the next state is returned.
 Example usage:
 def handle(self, msg):
 self._state = self._state(msg)
 """
 transitions = [(None, None, None)]
 def __init__(self, state_context):
 self._context = state_context
 def __call__(self, inp):
 new_state = self
 state_action_list = [(state, action) for cls, state,
 action in self.transitions if
 isinstance(inp, cls)]
 if 1 == len(state_action_list):
 state, action = state_action_list[0]
 if action is not None:
 try:
 action(inp)
 except TypeError:
 action(self, inp)
 new_state = state(self._context)
 elif 1 < len(state_action_list):
 raise AmbiguousTransitionError(inp, self)
 print ("[DEBUG] %s -- %s --> %s" %
 (self.__class__.__name__,
 inp.__class__.__name__,
 new_state.__class__.__name__ ))
 return new_state
class AmbiguousTransitionError(Exception):
 """Raised if more than one state transition was found for a
 given input
 """
 pass
################################################## Example
class MsgA(object): pass
class MsgB(object): pass
class MsgC(object): pass
class StateA(State): pass
class StateB(State):
 @staticmethod
 def print_msg(inp):
 print "Inp: ", inp
class StateC(State):
 def print_context(self, unused_inp):
 print "Context: ", self._context
StateA.transitions = [(MsgA, StateB, None)]
StateB.transitions = [(MsgB, StateC, StateB.print_msg)]
StateC.transitions = [(MsgB, StateB, None),
 (MsgC, StateA, StateC.print_context)]
class Context(object):
 def __init__(self):
 self._state = StateA(self)
 def handle(self, msg):
 self._state = self._state(msg)
if __name__ == "__main__":
 CONTEXT = Context()
 CONTEXT.handle(MsgA())
 CONTEXT.handle(MsgB())
 CONTEXT.handle(MsgC())
 CONTEXT.handle(MsgB())

The output looks like this:

python State.py

[DEBUG] StateA -- MsgA --> StateB
Inp: <__main__.MsgB object at 0x1eb6250>
[DEBUG] StateB -- MsgB --> StateC
Context: <__main__.Context object at 0x1eb6150>
[DEBUG] StateC -- MsgC --> StateA
[DEBUG] StateA -- MsgB --> StateA
asked Apr 13, 2014 at 13:05
\$\endgroup\$
2
  • \$\begingroup\$ Interesting question ! Out of curiosity, my understanding is that nothing happens when the transition is not possible. Is it something we want ? \$\endgroup\$ Commented Apr 13, 2014 at 16:33
  • \$\begingroup\$ Good question! I believe it depends on the field of use. For my purpose I usually need statemachines to simulate a behavior and dealing with many different messages. In this setting I don't want to repeat my code all the time. However it's easy to make the State throw an Exception if state_action_list has len 0. \$\endgroup\$ Commented Apr 13, 2014 at 16:36

1 Answer 1

1
\$\begingroup\$

Not a proper code review as per se but I was wondering if things couldn't be done in a more straight-forward way like this :

#!/usr/bin/python
class Machine(object):
 def __init__(self, state, transitions):
 self.transitions = transitions
 self.state = state
 def __call__(self, input):
 old_state = self.state
 self.state = self.transitions.get((self.state, input), self.state)
 print("DEBUG %s -- %s --> %s" % (old_state, input, self.state))
machine = Machine(
 "a",
 {
 ("a","msgA"):"b",
 ("b","msgB"):"c",
 ("c","msgB"):"b",
 ("c","msgC"):"a",
 }
 )
machine("msgA")
machine("msgB")
machine("msgC")
machine("msgB")

Using functions to have actual side effects, this could look like this :

#!/usr/bin/python
# examples of helper functions
def goto(s):
 return lambda state,input: s
def no_move():
 return lambda state,input: state
def verbose_no_move():
 def func(state,input):
 print('Cannot find a transition for %s from %s' % (input, state))
 return state
 return func
def print_input_and_goto(s):
 def func(state,input):
 print(input)
 return s
 return func
def print_old_state_and_goto(s):
 def func(state,input):
 print(state)
 return s
 return func
def raise_exception():
 def func(state,input):
 raise ValueError('Cannot find a transition for %s from %s' % (input, state))
class Machine(object):
 def __init__(self, initial_state, transitions):
 self.transitions = transitions
 self.state = initial_state
 self.default = verbose_no_move() # pick a default behavior
 def __call__(self, input):
 old_state = self.state
 func = self.transitions.get((self.state, input), self.default)
 self.state = func(self.state,input)
 print("DEBUG %s -- %s --> %s" % (old_state, input, self.state))
machine = Machine(
 "a",
 {
 ("a","msgA"):goto("b"),
 ("b","msgB"):print_input_and_goto("c"),
 ("c","msgB"):goto("b"),
 ("c","msgC"):print_old_state_and_goto("a"),
 }
 )
machine("msgA")
machine("msgB")
machine("msgC")
machine("msgB")
answered Apr 13, 2014 at 19:47
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Thank you Josay. Unfortunately I see a couple issues with this. As this implementation is not fully object oriented it has the typical drawbacks (such as, lack of data and logic encapsulation as well as failing late - at runtime) Another major issue is that a message can not carry any payload. Furthermore due to the lack of encapsulation any change to the state machine will be a change to everything, as you're changing in the Machine scope. Again, I appreciate your feedback and believe this is a valid solution for simple scripts but not a good basis for larger size programs. \$\endgroup\$ Commented Apr 14, 2014 at 5:55
  • \$\begingroup\$ Fair enough. That's a valid explanation to me :-) \$\endgroup\$ Commented Apr 14, 2014 at 6:33

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.