simplestates.py¶
Generic state machine class using iterators¶
Version: | 0.3 |
---|---|
Date: | 2006-12-01 |
Copyright: | 2006 Guenter Milde. Released under the terms of the GNU General Public License (v. 2 or later) |
Doctest string:
"""Simple generic state machine class using iterators
Usage
=====
Example: A two-state machine sorting numbers in the categories
"< 3" and ">= 3".
Preparation
-----------
Import the basic class::
>>> from simplestates import SimpleStates
Subclass and add state handlers:
>>> class StateExample(SimpleStates):
... def high_handler_generator(self):
... result = []
... for token in self.data_iterator:
... if token <= 3:
... self.state = "low"
... yield result
... result = []
... else:
... result.append(token)
... yield result
... def low_handler_generator(self):
... result = []
... for token in self.data_iterator:
... if token > 3:
... self.state = "high"
... yield result
... result = []
... else:
... result.append(token)
... yield result
Set up an instance of the StateExample machine with some test data::
>>> testdata = [1, 2, 3, 4, 5, 4, 3, 2, 1]
>>> testmachine = StateExample(testdata, state="low")
>>> print( [name for name in dir(testmachine) if name.endswith("generator")] )
['high_handler_generator', 'low_handler_generator']
Running
-------
Iterating over the state machine yields the results of state processing::
>>> for result in testmachine:
... print( result, end=',' )
...
[1, 2, 3],[5, 4],[2, 1],
For a correct working sort algorithm, we would expect::
[1, 2, 3] [4, 5, 4] [3, 2, 1]
However, to achieve this a backtracking algorithm is needed. See iterqueue.py
and simplestates-test.py for an example.
The `__call__` method returns a list of results. It is used if you call
an instance of the class::
>>> testmachine()
[[1, 2, 3], [5, 4], [2, 1]]
"""
Detailed documentation of this class and the design rationales (including tested variants) is available in the file simplestates-test.py.txt
This has been revised for Python3.
Abstract State Machine Class¶
class SimpleStates:
"""generic state machine acting on iterable data
Class attributes:
state -- name of the current state (next state_handler method called)
state_handler_generator_suffix -- common suffix of generator functions
returning a state-handler iterator
"""
state = 'start'
state_handler_generator_suffix = "_handler_generator"
Initialisation¶
sets the data object to the data argument.
remaining keyword arguments are stored as class attributes (or methods, if they are function objects) overwriting class defaults (a neat little trick I found somewhere on the net)
- ..note: This is the same as self.__dict__.update(keyw). However,
the “Tutorial” advises to confine the direct use of __dict__ to post-mortem analysis or the like…
def __init__(self, data, **keyw):
"""data -- iterable data object
(list, file, generator, string, ...)
**keyw -- all remaining keyword arguments are
stored as class attributes
"""
self.data = data
for (key, value) in keyw.items():
setattr(self, key, value)
Iteration over class instances¶
The special __iter__ method returns an iterator. This allows to use a class instance directly in an iteration loop. We define it as is a generator method that sets the initial state and then iterates over the data calling the state methods:
def __iter__(self):
"""Generate and return an iterator
* ensure `data` is an iterator
* convert the state generators into iterators
* (re) set the state attribute to the initial state
* pass control to the active states state_handler
which should call and process next(self.data_iterator)
"""
self.data_iterator = iter(self.data)
self._initialize_state_generators()
# now start the iteration
while True:
yield getattr(self, self.state)()
a helper function generates state handlers from generators. It is called by the __iter__ method above:
def _initialize_state_generators(self):
"""Generic function to initialise state handlers from generators
functions whose name matches `[^_]<state>_handler_generator` will
be converted to iterators and their `.__next__()` method stored as
`self.<state>`.
"""
suffix = self.state_handler_generator_suffix
shg_names = [name for name in dir(self)
if name.endswith(suffix)
and not name.startswith("_")]
for name in shg_names:
shg = getattr(self, name)
setattr(self, name[:-len(suffix)], shg().__next__ )
Use instances like functions¶
To allow use of class instances as callable objects, we add a __call__ method:
def __call__(self):
"""Iterate over state-machine and return results as a list"""
return [token for token in self]
Command line usage¶
running this script does a doctest:
if __name__ == "__main__":
import doctest
doctest.testmod()