web-dev-qa-db-fra.com

Python conception de machine à états

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.

45
jldupont

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".

39
S.Lott

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.)

12
PaulMcG

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).

8
Trent

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
5
Jonathan

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",
    #...
  }
3
MAK

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.

2
Iwan LD

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 ...

2
gecco

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 .

2
Ben Karel

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}
2
cmcginty

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.

1
Jason Orendorff

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 .

0
Alexey

Autres projets connexes:

Vous pouvez peindre la machine d'état, puis l'utiliser dans votre code.

0
Veselin Penev