En relation avec cette question de débordement de pile (conception de la machine à états C), pourriez-vous partager les gens de votre débordement de pile Python techniques de conception de machines à états avec moi (et la communauté)?
En ce moment, je vais pour un moteur basé sur ce qui suit:
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
Mais je suis sûr qu'il existe des tonnes de façons de procéder tout en tirant parti de la nature dynamique de Python (par exemple, la répartition dynamique).
Je recherche des techniques de conception pour le "moteur" qui reçoit les "événements" et les "dépêches" contre celles basées sur "l'état" de la machine.
Je ne comprends pas vraiment la question. L'état State Design est assez clair. Voir le 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()
C'est un passe-partout assez courant, utilisé en Java, C++, Python (et je suis sûr que d'autres langages aussi)).
Si vos règles de transition d'état s'avèrent triviales, il existe des optimisations pour pousser la règle de transition elle-même dans la superclasse.
Notez que nous devons avoir des références directes, donc nous faisons référence aux classes par nom, et utilisons eval
pour traduire un nom de classe en une classe réelle. L'alternative consiste à créer des variables d'instance de règles de transition au lieu de variables de classe, puis à créer les instances une fois toutes les classes définies.
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" }
Dans certains cas, votre événement n'est pas aussi simple que de tester l'égalité des objets, donc une règle de transition plus générale consiste à utiliser une liste appropriée de paires fonction-objet.
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") ]
Étant donné que les règles sont évaluées séquentiellement, cela permet une règle "par défaut".
Dans le numéro d'avril 2009 de Python Magazine, j'ai écrit un article sur l'incorporation d'un état DSL dans Python, en utilisant pyparsing et imputil. Ce code vous permettrait d'écrire le module trafficLight.pystate:
# trafficLight.pystate
# define state machine
statemachine TrafficLight:
Red -> Green
Green -> Yellow
Yellow -> Red
# define some class level constants
Red.carsCanGo = False
Yellow.carsCanGo = True
Green.carsCanGo = True
Red.delay = wait(20)
Yellow.delay = wait(3)
Green.delay = wait(15)
et le compilateur DSL créerait toutes les classes TrafficLight, Red, Yellow et Green nécessaires, ainsi que les méthodes de transition d'état appropriées. Le code pourrait appeler ces classes en utilisant quelque chose comme ceci:
import statemachine
import trafficLight
tl = trafficLight.Red()
for i in range(6):
print tl, "GO" if tl.carsCanGo else "STOP"
tl.delay()
tl = tl.next_state()
(Malheureusement, imputil a été supprimé dans Python 3.)
Il y a ce modèle de conception pour utiliser des décorateurs pour implémenter des machines d'état. De la description sur la page:
Les décorateurs sont utilisés pour spécifier les méthodes qui sont les gestionnaires d'événements pour la classe.
Il y a aussi un exemple de code sur la page (il est assez long donc je ne le collerai pas ici).
Je n'étais pas non plus satisfait des options actuelles pour state_machines, j'ai donc écrit la bibliothèque state_machine .
Vous pouvez l'installer par pip install state_machine
et utilisez-le comme ceci:
@acts_as_state_machine
class Person():
name = 'Billy'
sleeping = State(initial=True)
running = State()
cleaning = State()
run = Event(from_states=sleeping, to_state=running)
cleanup = Event(from_states=running, to_state=cleaning)
sleep = Event(from_states=(running, cleaning), to_state=sleeping)
@before('sleep')
def do_one_thing(self):
print "{} is sleepy".format(self.name)
@before('sleep')
def do_another_thing(self):
print "{} is REALLY sleepy".format(self.name)
@after('sleep')
def snore(self):
print "Zzzzzzzzzzzz"
@after('sleep')
def big_snore(self):
print "Zzzzzzzzzzzzzzzzzzzzzz"
person = Person()
print person.current_state == person.sleeping # True
print person.is_sleeping # True
print person.is_running # False
person.run()
print person.is_running # True
person.sleep()
# Billy is sleepy
# Billy is REALLY sleepy
# Zzzzzzzzzzzz
# Zzzzzzzzzzzzzzzzzzzzzz
print person.is_sleeping # True
Je pense que la réponse de S. Lott est une bien meilleure façon d'implémenter une machine à états, mais si vous voulez continuer avec votre approche, utilisez (state,event)
comme clé pour votre dict
est mieux. Modification de votre code:
class HandlerFsm(object):
_fsm = {
("state_a","event"): "next_state",
#...
}
Je ne recommanderais certainement pas de mettre en œuvre vous-même un modèle aussi connu. Optez simplement pour une implémentation open source comme transitions et encapsulez une autre classe si vous avez besoin de fonctionnalités personnalisées. Dans cet article j'explique pourquoi je préfère cette implémentation particulière et ses fonctionnalités.
Je pense que l'outil PySCXML doit également être examiné de plus près.
Ce projet utilise la définition du W3C: State Chart XML (SCXML) : State Machine Notation for Control Abstraction
SCXML fournit un environnement d'exécution générique basé sur une machine à états basé sur CCXML et Harel State Tables
Actuellement, SCXML est un projet de travail; mais il y a de fortes chances qu'il obtienne bientôt une recommandation du W3C (il s'agit du 9ème projet).
Un autre point intéressant à souligner est qu'il existe un projet Apache Commons visant à créer et à maintenir un moteur Java SCXML capable d'exécuter une machine d'état définie à l'aide d'un document SCXML, tout en faisant abstraction des interfaces d'environnement. ..
Et pour certains autres outils, la prise en charge de cette technologie émergera à l'avenir lorsque SCXML quittera son statut de brouillon ...
Cela dépend probablement de la complexité de votre machine d'état. Pour les machines à états simples, un dict de dict (des clés d'événement aux clés d'état pour les DFA, ou des clés d'événement aux listes/ensembles/tuples de clés d'état pour les NFA) sera probablement la chose la plus simple à écrire et à comprendre.
Pour les machines à états plus complexes, j'ai entendu de bonnes choses à propos de SMC , qui peut compiler des descriptions de machines à états déclaratives à coder dans une grande variété de langages, y compris Python .
Le code suivant est une solution vraiment simple. La seule partie intéressante est:
def next_state(self,cls):
self.__class__ = cls
Toute la logique de chaque état est contenue dans une classe distincte. L '"état" est modifié en remplaçant le " __ class __ " de l'instance en cours d'exécution.
#!/usr/bin/env python
class State(object):
call = 0 # shared state variable
def next_state(self,cls):
print '-> %s' % (cls.__name__,),
self.__class__ = cls
def show_state(self,i):
print '%2d:%2d:%s' % (self.call,i,self.__class__.__name__),
class State1(State):
__call = 0 # state variable
def __call__(self,ok):
self.show_state(self.__call)
self.call += 1
self.__call += 1
# transition
if ok: self.next_state(State2)
print '' # force new line
class State2(State):
__call = 0
def __call__(self,ok):
self.show_state(self.__call)
self.call += 1
self.__call += 1
# transition
if ok: self.next_state(State3)
else: self.next_state(State1)
print '' # force new line
class State3(State):
__call = 0
def __call__(self,ok):
self.show_state(self.__call)
self.call += 1
self.__call += 1
# transition
if not ok: self.next_state(State2)
print '' # force new line
if __== '__main__':
sm = State1()
for v in [1,1,1,0,0,0,1,1,0,1,1,0,0,1,0,0,1,0,0]:
sm(v)
print '---------'
print vars(sm
Résultat:
0: 0:State1 -> State2
1: 0:State2 -> State3
2: 0:State3
3: 1:State3 -> State2
4: 1:State2 -> State1
5: 1:State1
6: 2:State1 -> State2
7: 2:State2 -> State3
8: 2:State3 -> State2
9: 3:State2 -> State3
10: 3:State3
11: 4:State3 -> State2
12: 4:State2 -> State1
13: 3:State1 -> State2
14: 5:State2 -> State1
15: 4:State1
16: 5:State1 -> State2
17: 6:State2 -> State1
18: 6:State1
---------
{'_State1__call': 7, 'call': 19, '_State3__call': 5, '_State2__call': 7}
Je ne penserais pas à atteindre une machine à états finis pour gérer XML. La façon habituelle de le faire, je pense, est d'utiliser une pile:
class TrackInfoHandler(object):
def __init__(self):
self._stack=[]
## ================================== Event callbacks
def startElement(self, name, attrs):
cls = self.elementClasses[name]
self._stack.append(cls(**attrs))
def characters(self, ch):
self._stack[-1].addCharacters(ch)
def endElement(self, name):
e = self._stack.pop()
e.close()
if self._stack:
self._stack[-1].addElement(e)
Pour chaque type d'élément, vous avez juste besoin d'une classe qui prend en charge les méthodes addCharacters
, addElement
et close
.
EDIT: Pour clarifier, oui, je veux dire que les machines à états finis sont généralement la mauvaise réponse, qu'en tant que technique de programmation à usage général, ce sont des ordures et que vous devriez rester à l'écart.
Il y a quelques problèmes vraiment bien compris et clairement définis pour lesquels les FSM sont une bonne solution. Lex
, par exemple, est une bonne chose.
Cela dit, les FSM ne font généralement pas bien face au changement. Supposons qu'un jour vous vouliez ajouter un peu d'état, peut-être un "avons-nous déjà vu l'élément X?" drapeau. Dans le code ci-dessus, vous ajoutez un attribut booléen à la classe d'élément appropriée et vous avez terminé. Dans une machine à états finis, vous doublez le nombre d'états et de transitions.
Les problèmes qui nécessitent un état fini au début évoluent très souvent pour nécessiter encore plus d'état, comme peut-être un nombre , auquel cas votre schéma FSM est toast, ou pire, vous le transformez en une sorte de machine à états généralisée, et à ce stade, vous êtes vraiment en difficulté. Plus vous allez loin, plus vos règles commencent à agir comme du code - mais du code dans un langage interprété lentement que vous avez inventé et que personne d'autre ne connaît, pour lequel il n'y a pas de débogueur ni d'outils.
Voici une solution pour les "objets d'état" que j'ai trouvée, mais elle est plutôt inefficace pour votre objectif, car les changements d'état sont relativement coûteux. Cependant, cela peut bien fonctionner pour les objets qui changent rarement d'état ou ne subissent qu'un nombre limité de changements d'état. L'avantage est qu'une fois l'état modifié, il n'y a plus d'indirection redondante.
class T:
"""
Descendant of `object` that rectifies `__new__` overriding.
This class is intended to be listed as the last base class (just
before the implicit `object`). It is a part of a workaround for
* https://bugs.python.org/issue36827
"""
@staticmethod
def __new__(cls, *_args, **_kwargs):
return object.__new__(cls)
class Stateful:
"""
Abstract base class (or mixin) for "stateful" classes.
Subclasses must implement `InitState` mixin.
"""
@staticmethod
def __new__(cls, *args, **kwargs):
# XXX: see https://stackoverflow.com/a/9639512
class CurrentStateProxy(cls.InitState):
@staticmethod
def _set_state(state_cls=cls.InitState):
__class__.__bases__ = (state_cls,)
class Eigenclass(CurrentStateProxy, cls):
__new__ = None # just in case
return super(__class__, cls).__new__(Eigenclass, *args, **kwargs)
# XXX: see https://bugs.python.org/issue36827 for the reason for `T`.
class StatefulThing(Stateful, T):
class StateA:
"""First state mixin."""
def say_hello(self):
self._say("Hello!")
self.hello_count += 1
self._set_state(self.StateB)
return True
def say_goodbye(self):
self._say("Another goodbye?")
return False
class StateB:
"""Second state mixin."""
def say_hello(self):
self._say("Another hello?")
return False
def say_goodbye(self):
self._say("Goodbye!")
self.goodbye_count += 1
self._set_state(self.StateA)
return True
# This one is required by `Stateful`.
class InitState(StateA):
"""Third state mixin -- the initial state."""
def say_goodbye(self):
self._say("Why?")
return False
def __init__(self, name):
self.name = name
self.hello_count = self.goodbye_count = 0
def _say(self, message):
print("{}: {}".format(self.name, message))
def say_hello_followed_by_goodbye(self):
self.say_hello() and self.say_goodbye()
# ----------
# ## Demo ##
# ----------
if __== "__main__":
t1 = StatefulThing("t1")
t2 = StatefulThing("t2")
print("> t1, say hello.")
t1.say_hello()
print("> t2, say goodbye.")
t2.say_goodbye()
print("> t2, say hello.")
t2.say_hello()
print("> t1, say hello.")
t1.say_hello()
print("> t1, say hello followed by goodbye.")
t1.say_hello_followed_by_goodbye()
print("> t2, say goodbye.")
t2.say_goodbye()
print("> t2, say hello followed by goodbye.")
t2.say_hello_followed_by_goodbye()
print("> t1, say goodbye.")
t1.say_goodbye()
print("> t2, say hello.")
t2.say_hello()
print("---")
print( "t1 said {} hellos and {} goodbyes."
.format(t1.hello_count, t1.goodbye_count) )
print( "t2 said {} hellos and {} goodbyes."
.format(t2.hello_count, t2.goodbye_count) )
# Expected output:
#
# > t1, say hello.
# t1: Hello!
# > t2, say goodbye.
# t2: Why?
# > t2, say hello.
# t2: Hello!
# > t1, say hello.
# t1: Another hello?
# > t1, say hello followed by goodbye.
# t1: Another hello?
# > t2, say goodbye.
# t2: Goodbye!
# > t2, say hello followed by goodbye.
# t2: Hello!
# t2: Goodbye!
# > t1, say goodbye.
# t1: Goodbye!
# > t2, say hello.
# t2: Hello!
# ---
# t1 said 1 hellos and 1 goodbyes.
# t2 said 3 hellos and 2 goodbyes.
J'ai posté une "demande de remarques" ici .
Autres projets connexes:
Vous pouvez peindre la machine d'état, puis l'utiliser dans votre code.