Une implémentation légère et orientée objet d'une machine à états en Python avec de nombreuses extensions. Compatible avec Python 2.7+ et 3.0+.
pip install transitions
... ou clonez le dépôt depuis GitHub puis :
python setup.py install
On dit qu’un bon exemple vaut 100 pages de documentation API, un million de directives ou mille mots.
Eh bien, "ils" mentent probablement... mais voici quand même un exemple :
from transitions import Machine
import random
class NarcolepticSuperhero ( object ):
# Define some states. Most of the time, narcoleptic superheroes are just like
# everyone else. Except for...
states = [ 'asleep' , 'hanging out' , 'hungry' , 'sweaty' , 'saving the world' ]
def __init__ ( self , name ):
# No anonymous superheroes on my watch! Every narcoleptic superhero gets
# a name. Any name at all. SleepyMan. SlumberGirl. You get the idea.
self . name = name
# What have we accomplished today?
self . kittens_rescued = 0
# Initialize the state machine
self . machine = Machine ( model = self , states = NarcolepticSuperhero . states , initial = 'asleep' )
# Add some transitions. We could also define these using a static list of
# dictionaries, as we did with states above, and then pass the list to
# the Machine initializer as the transitions= argument.
# At some point, every superhero must rise and shine.
self . machine . add_transition ( trigger = 'wake_up' , source = 'asleep' , dest = 'hanging out' )
# Superheroes need to keep in shape.
self . machine . add_transition ( 'work_out' , 'hanging out' , 'hungry' )
# Those calories won't replenish themselves!
self . machine . add_transition ( 'eat' , 'hungry' , 'hanging out' )
# Superheroes are always on call. ALWAYS. But they're not always
# dressed in work-appropriate clothing.
self . machine . add_transition ( 'distress_call' , '*' , 'saving the world' ,
before = 'change_into_super_secret_costume' )
# When they get off work, they're all sweaty and disgusting. But before
# they do anything else, they have to meticulously log their latest
# escapades. Because the legal department says so.
self . machine . add_transition ( 'complete_mission' , 'saving the world' , 'sweaty' ,
after = 'update_journal' )
# Sweat is a disorder that can be remedied with water.
# Unless you've had a particularly long day, in which case... bed time!
self . machine . add_transition ( 'clean_up' , 'sweaty' , 'asleep' , conditions = [ 'is_exhausted' ])
self . machine . add_transition ( 'clean_up' , 'sweaty' , 'hanging out' )
# Our NarcolepticSuperhero can fall asleep at pretty much any time.
self . machine . add_transition ( 'nap' , '*' , 'asleep' )
def update_journal ( self ):
""" Dear Diary, today I saved Mr. Whiskers. Again. """
self . kittens_rescued += 1
@ property
def is_exhausted ( self ):
""" Basically a coin toss. """
return random . random () < 0.5
def change_into_super_secret_costume ( self ):
print ( "Beauty, eh?" )
Voilà, vous avez maintenant intégré une machine à états dans NarcolepticSuperhero
. Allons faire un tour avec lui...
> >> batman = NarcolepticSuperhero ( "Batman" )
> >> batman . state
'asleep'
> >> batman . wake_up ()
> >> batman . state
'hanging out'
> >> batman . nap ()
> >> batman . state
'asleep'
> >> batman . clean_up ()
MachineError : "Can't trigger event clean_up from state asleep!"
> >> batman . wake_up ()
> >> batman . work_out ()
> >> batman . state
'hungry'
# Batman still hasn't done anything useful...
> >> batman . kittens_rescued
0
# We now take you live to the scene of a horrific kitten entreement...
> >> batman . distress_call ()
'Beauty, eh?'
> >> batman . state
'saving the world'
# Back to the crib.
> >> batman . complete_mission ()
> >> batman . state
'sweaty'
> >> batman . clean_up ()
> >> batman . state
'asleep' # Too tired to shower!
# Another productive day, Alfred.
> >> batman . kittens_rescued
1
Bien que nous ne puissions pas lire dans l'esprit du véritable Batman, nous pouvons sûrement visualiser l'état actuel de notre NarcolepticSuperhero
.
Jetez un œil aux extensions Diagrams si vous voulez savoir comment procéder.
Une machine à états est un modèle de comportement composé d’un nombre fini d’ états et de transitions entre ces états. Dans chaque état et transition, certaines actions peuvent être effectuées. Une machine à états doit démarrer à un certain état initial . Lors de l'utilisation transitions
, une machine à états peut être constituée de plusieurs objets dont certains ( machines ) contiennent des définitions pour la manipulation d'autres ( modèles ). Ci-dessous, nous examinerons quelques concepts de base et comment les utiliser.
État . Un état représente une condition ou une étape particulière dans la machine à états. Il s'agit d'un mode de comportement ou d'une phase distincte dans un processus.
Transition . Il s'agit du processus ou de l'événement qui fait passer la machine à états d'un état à un autre.
Modèle . La structure avec état réelle. C'est l'entité qui est mise à jour lors des transitions. Il peut également définir des actions qui seront exécutées lors des transitions. Par exemple, juste avant une transition ou lorsqu’on entre ou sort d’un état.
Machine . Il s'agit de l'entité qui gère et contrôle le modèle, les états, les transitions et les actions. C'est le chef d'orchestre qui orchestre tout le processus de la machine à états.
Déclenchement . C'est l'événement qui initie une transition, la méthode qui envoie le signal pour démarrer une transition.
Action . Opération ou tâche spécifique effectuée lorsqu'un certain état est entré, quitté ou pendant une transition. L'action est implémentée via des rappels , qui sont des fonctions qui sont exécutées lorsqu'un événement se produit.
Faire fonctionner une machine à états est assez simple. Supposons que vous ayez l'objet lump
(une instance de la classe Matter
) et que vous souhaitiez gérer ses états :
class Matter ( object ):
pass
lump = Matter ()
Vous pouvez initialiser une machine à états de travail ( minimale ) liée au modèle lump
comme ceci :
from transitions import Machine
machine = Machine ( model = lump , states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ], initial = 'solid' )
# Lump now has a new state attribute!
lump . state
> >> 'solid'
Une alternative consiste à ne pas transmettre explicitement un modèle à l'initialiseur Machine
:
machine = Machine ( states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ], initial = 'solid' )
# The machine instance itself now acts as a model
machine . state
> >> 'solid'
Notez que cette fois je n'ai pas passé le modèle lump
comme argument. Le premier argument transmis à Machine
fait office de modèle. Ainsi, lorsque je passe quelque chose là-bas, toutes les fonctions pratiques seront ajoutées à l'objet. Si aucun modèle n'est fourni, l'instance machine
elle-même fait office de modèle.
Quand au début j'ai dit "minimal", c'est parce que si cette machine à états est techniquement opérationnelle, elle ne fait en réalité rien. Il commence à l'état 'solid'
, mais ne passera jamais à un autre état, car aucune transition n'est définie... pour l'instant !
Essayons encore.
# The states
states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ]
# And some transitions between states. We're lazy, so we'll leave out
# the inverse phase transitions (freezing, condensation, etc.).
transitions = [
{ 'trigger' : 'melt' , 'source' : 'solid' , 'dest' : 'liquid' },
{ 'trigger' : 'evaporate' , 'source' : 'liquid' , 'dest' : 'gas' },
{ 'trigger' : 'sublimate' , 'source' : 'solid' , 'dest' : 'gas' },
{ 'trigger' : 'ionize' , 'source' : 'gas' , 'dest' : 'plasma' }
]
# Initialize
machine = Machine ( lump , states = states , transitions = transitions , initial = 'liquid' )
# Now lump maintains state...
lump . state
> >> 'liquid'
# And that state can change...
# Either calling the shiny new trigger methods
lump . evaporate ()
lump . state
> >> 'gas'
# Or by calling the trigger method directly
lump . trigger ( 'ionize' )
lump . state
> >> 'plasma'
Notez les nouvelles méthodes brillantes attachées à l'instance Matter
( evaporate()
, ionize()
, etc.). Chaque méthode déclenche la transition correspondante. Les transitions peuvent également être déclenchées dynamiquement en appelant la méthode trigger()
fournie avec le nom de la transition, comme indiqué ci-dessus. Plus d’informations à ce sujet dans la section Déclencher une transition.
L’âme de toute bonne machine à États (et de nombreuses mauvaises, sans aucun doute) est un ensemble d’États. Ci-dessus, nous avons défini les états de modèle valides en transmettant une liste de chaînes à l'initialiseur Machine
. Mais en interne, les États sont en réalité représentés comme des objets State
.
Vous pouvez initialiser et modifier les états de plusieurs manières. Concrètement, vous pouvez :
Machine
donnant le(s) nom(s) du ou des états, ouState
, ouLes extraits suivants illustrent plusieurs façons d’atteindre le même objectif :
# import Machine and State class
from transitions import Machine , State
# Create a list of 3 states to pass to the Machine
# initializer. We can mix types; in this case, we
# pass one State, one string, and one dict.
states = [
State ( name = 'solid' ),
'liquid' ,
{ 'name' : 'gas' }
]
machine = Machine ( lump , states )
# This alternative example illustrates more explicit
# addition of states and state callbacks, but the net
# result is identical to the above.
machine = Machine ( lump )
solid = State ( 'solid' )
liquid = State ( 'liquid' )
gas = State ( 'gas' )
machine . add_states ([ solid , liquid , gas ])
Les états sont initialisés une fois lorsqu'ils sont ajoutés à la machine et persisteront jusqu'à ce qu'ils en soient supprimés. En d’autres termes : si vous modifiez les attributs d’un objet d’état, cette modification ne sera PAS réinitialisée la prochaine fois que vous entrerez dans cet état. Découvrez comment étendre les fonctionnalités d'état au cas où vous auriez besoin d'un autre comportement.
Mais le simple fait d'avoir des états et de pouvoir se déplacer entre eux (transitions) n'est pas très utile en soi. Que se passe-t-il si vous souhaitez faire quelque chose, effectuer une action lorsque vous entrez ou sortez d'un état ? C'est là qu'interviennent les rappels .
Un State
peut également être associé à une liste de rappels enter
et exit
, qui sont appelés chaque fois que la machine à états entre ou quitte cet état. Vous pouvez spécifier des rappels lors de l'initialisation en les transmettant à un constructeur d'objet State
, dans un dictionnaire de propriétés d'état, ou en les ajoutant ultérieurement.
Pour plus de commodité, chaque fois qu'un nouveau State
est ajouté à une Machine
, les méthodes on_enter_«state name»
et on_exit_«state name»
sont créées dynamiquement sur la Machine (pas sur le modèle !), ce qui vous permet d'ajouter dynamiquement de nouvelles entrées et sorties. rappels plus tard si vous en avez besoin.
# Our old Matter class, now with a couple of new methods we
# can trigger when entering or exit states.
class Matter ( object ):
def say_hello ( self ): print ( "hello, new state!" )
def say_goodbye ( self ): print ( "goodbye, old state!" )
lump = Matter ()
# Same states as above, but now we give StateA an exit callback
states = [
State ( name = 'solid' , on_exit = [ 'say_goodbye' ]),
'liquid' ,
{ 'name' : 'gas' , 'on_exit' : [ 'say_goodbye' ]}
]
machine = Machine ( lump , states = states )
machine . add_transition ( 'sublimate' , 'solid' , 'gas' )
# Callbacks can also be added after initialization using
# the dynamically added on_enter_ and on_exit_ methods.
# Note that the initial call to add the callback is made
# on the Machine and not on the model.
machine . on_enter_gas ( 'say_hello' )
# Test out the callbacks...
machine . set_state ( 'solid' )
lump . sublimate ()
> >> 'goodbye, old state!'
> >> 'hello, new state!'
Notez que le rappel on_enter_«state name»
ne se déclenchera pas lors de la première initialisation d'une machine. Par exemple, si vous avez défini un rappel on_enter_A()
et que vous initialisez la Machine
avec initial='A'
, on_enter_A()
ne sera déclenché qu'à la prochaine fois que vous entrerez dans l'état A
. (Si vous devez vous assurer que on_enter_A()
se déclenche à l'initialisation, vous pouvez simplement créer un état initial factice, puis appeler explicitement to_A()
dans la méthode __init__
.)
En plus de transmettre des rappels lors de l'initialisation d'un State
ou de les ajouter dynamiquement, il est également possible de définir des rappels dans la classe de modèle elle-même, ce qui peut augmenter la clarté du code. Par exemple:
class Matter ( object ):
def say_hello ( self ): print ( "hello, new state!" )
def say_goodbye ( self ): print ( "goodbye, old state!" )
def on_enter_A ( self ): print ( "We've just entered state A!" )
lump = Matter ()
machine = Machine ( lump , states = [ 'A' , 'B' , 'C' ])
Désormais, chaque fois que lump
passe à l'état A
, la méthode on_enter_A()
définie dans la classe Matter
se déclenchera.
Vous pouvez utiliser les rappels on_final
qui seront déclenchés lorsqu'un état avec final=True
est entré.
from transitions import Machine , State
states = [ State ( name = 'idling' ),
State ( name = 'rescuing_kitten' ),
State ( name = 'offender_gone' , final = True ),
State ( name = 'offender_caught' , final = True )]
transitions = [[ "called" , "idling" , "rescuing_kitten" ], # we will come when called
{ "trigger" : "intervene" ,
"source" : "rescuing_kitten" ,
"dest" : "offender_gone" , # we
"conditions" : "offender_is_faster" }, # unless they are faster
[ "intervene" , "rescuing_kitten" , "offender_caught" ]]
class FinalSuperhero ( object ):
def __init__ ( self , speed ):
self . machine = Machine ( self , states = states , transitions = transitions , initial = "idling" , on_final = "claim_success" )
self . speed = speed
def offender_is_faster ( self , offender_speed ):
return self . speed < offender_speed
def claim_success ( self , ** kwargs ):
print ( "The kitten is safe." )
hero = FinalSuperhero ( speed = 10 ) # we are not in shape today
hero . called ()
assert hero . is_rescuing_kitten ()
hero . intervene ( offender_speed = 15 )
# >>> 'The kitten is safe'
assert hero . machine . get_state ( hero . state ). final # it's over
assert hero . is_offender_gone () # maybe next time ...
Vous pouvez toujours vérifier l'état actuel du modèle en :
.state
, ouis_«state name»()
Et si vous souhaitez récupérer l'objet State
réel pour l'état actuel, vous pouvez le faire via la méthode get_state()
de l'instance Machine
.
lump . state
> >> 'solid'
lump . is_gas ()
> >> False
lump . is_solid ()
> >> True
machine . get_state ( lump . state ). name
> >> 'solid'
Si vous le souhaitez, vous pouvez choisir votre propre nom d'attribut d'état en passant l'argument model_attribute
lors de l'initialisation du Machine
. Cela changera également le nom de is_«state name»()
en is_«model_attribute»_«state name»()
. De même, les transitions automatiques seront nommées to_«model_attribute»_«state name»()
au lieu de to_«state name»()
. Ceci est fait pour permettre à plusieurs machines de travailler sur le même modèle avec des noms d'attributs d'état individuels.
lump = Matter ()
machine = Machine ( lump , states = [ 'solid' , 'liquid' , 'gas' ], model_attribute = 'matter_state' , initial = 'solid' )
lump . matter_state
> >> 'solid'
# with a custom 'model_attribute', states can also be checked like this:
lump . is_matter_state_solid ()
> >> True
lump . to_matter_state_gas ()
> >> True
Jusqu’à présent, nous avons vu comment donner des noms d’état et utiliser ces noms pour travailler avec notre machine à états. Si vous préférez une saisie plus stricte et une complétion plus complète du code IDE (ou si vous ne pouvez tout simplement plus taper « sesquipedalophobia » parce que le mot vous fait peur), l'utilisation des énumérations pourrait être ce que vous recherchez :
import enum # Python 2.7 users need to have 'enum34' installed
from transitions import Machine
class States ( enum . Enum ):
ERROR = 0
RED = 1
YELLOW = 2
GREEN = 3
transitions = [[ 'proceed' , States . RED , States . YELLOW ],
[ 'proceed' , States . YELLOW , States . GREEN ],
[ 'error' , '*' , States . ERROR ]]
m = Machine ( states = States , transitions = transitions , initial = States . RED )
assert m . is_RED ()
assert m . state is States . RED
state = m . get_state ( States . RED ) # get transitions.State object
print ( state . name ) # >>> RED
m . proceed ()
m . proceed ()
assert m . is_GREEN ()
m . error ()
assert m . state is States . ERROR
Vous pouvez mélanger des énumérations et des chaînes si vous le souhaitez (par exemple [States.RED, 'ORANGE', States.YELLOW, States.GREEN]
) mais notez qu'en interne, transitions
géreront toujours les états par nom ( enum.Enum.name
). Ainsi, il n'est pas possible d'avoir les états 'GREEN'
et States.GREEN
en même temps.
Certains des exemples ci-dessus illustrent déjà l'utilisation des transitions en passant, mais nous les explorerons ici plus en détail.
Comme pour les états, chaque transition est représentée en interne comme son propre objet – une instance de la classe Transition
. Le moyen le plus rapide d’initialiser un ensemble de transitions consiste à transmettre un dictionnaire, ou une liste de dictionnaires, à l’initialiseur Machine
. Nous avons déjà vu cela ci-dessus :
transitions = [
{ 'trigger' : 'melt' , 'source' : 'solid' , 'dest' : 'liquid' },
{ 'trigger' : 'evaporate' , 'source' : 'liquid' , 'dest' : 'gas' },
{ 'trigger' : 'sublimate' , 'source' : 'solid' , 'dest' : 'gas' },
{ 'trigger' : 'ionize' , 'source' : 'gas' , 'dest' : 'plasma' }
]
machine = Machine ( model = Matter (), states = states , transitions = transitions )
Définir des transitions dans des dictionnaires présente l’avantage de la clarté, mais peut s’avérer fastidieux. Si vous recherchez la brièveté, vous pouvez choisir de définir des transitions à l'aide de listes. Assurez-vous simplement que les éléments de chaque liste sont dans le même ordre que les arguments de position dans l'initialisation Transition
(c'est-à-dire trigger
, source
, destination
, etc.).
La liste de listes suivante est fonctionnellement équivalente à la liste de dictionnaires ci-dessus :
transitions = [
[ 'melt' , 'solid' , 'liquid' ],
[ 'evaporate' , 'liquid' , 'gas' ],
[ 'sublimate' , 'solid' , 'gas' ],
[ 'ionize' , 'gas' , 'plasma' ]
]
Alternativement, vous pouvez ajouter des transitions à une Machine
après l'initialisation :
machine = Machine ( model = lump , states = states , initial = 'solid' )
machine . add_transition ( 'melt' , source = 'solid' , dest = 'liquid' )
Pour qu’une transition soit exécutée, un événement doit la déclencher . Il existe deux manières de procéder :
À l'aide de la méthode d'attachement automatique dans le modèle de base :
> >> lump . melt ()
> >> lump . state
'liquid'
> >> lump . evaporate ()
> >> lump . state
'gas'
Notez que vous n'avez pas besoin de définir explicitement ces méthodes nulle part ; le nom de chaque transition est lié au modèle passé à l'initialiseur Machine
(dans ce cas, lump
). Cela signifie également que votre modèle ne doit pas déjà contenir des méthodes portant le même nom que les déclencheurs d'événements, car transitions
n'attacheront des méthodes pratiques à votre modèle que si la place n'est pas déjà occupée. Si vous souhaitez modifier ce comportement, consultez la FAQ.
En utilisant la méthode trigger
, désormais attachée à votre modèle (s'il n'y était pas auparavant). Cette méthode vous permet d'exécuter des transitions par nom au cas où un déclenchement dynamique serait requis :
> >> lump . trigger ( 'melt' )
> >> lump . state
'liquid'
> >> lump . trigger ( 'evaporate' )
> >> lump . state
'gas'
Par défaut, le déclenchement d'une transition invalide déclenchera une exception :
> >> lump . to_gas ()
> >> # This won't work because only objects in a solid state can melt
>> > lump . melt ()
transitions . core . MachineError : "Can't trigger event melt from state gas!"
Ce comportement est généralement souhaitable, car il permet de vous alerter des problèmes dans votre code. Mais dans certains cas, vous souhaiterez peut-être ignorer silencieusement les déclencheurs non valides. Vous pouvez le faire en définissant ignore_invalid_triggers=True
(soit état par état, soit globalement pour tous les états) :
> >> # Globally suppress invalid trigger exceptions
>> > m = Machine ( lump , states , initial = 'solid' , ignore_invalid_triggers = True )
> >> # ...or suppress for only one group of states
>> > states = [ 'new_state1' , 'new_state2' ]
> >> m . add_states ( states , ignore_invalid_triggers = True )
> >> # ...or even just for a single state. Here, exceptions will only be suppressed when the current state is A.
>> > states = [ State ( 'A' , ignore_invalid_triggers = True ), 'B' , 'C' ]
> >> m = Machine ( lump , states )
> >> # ...this can be inverted as well if just one state should raise an exception
>> > # since the machine's global value is not applied to a previously initialized state.
>> > states = [ 'A' , 'B' , State ( 'C' )] # the default value for 'ignore_invalid_triggers' is False
> >> m = Machine ( lump , states , ignore_invalid_triggers = True )
Si vous avez besoin de savoir quelles transitions sont valides à partir d'un certain état, vous pouvez utiliser get_triggers
:
m . get_triggers ( 'solid' )
> >> [ 'melt' , 'sublimate' ]
m . get_triggers ( 'liquid' )
> >> [ 'evaporate' ]
m . get_triggers ( 'plasma' )
> >> []
# you can also query several states at once
m . get_triggers ( 'solid' , 'liquid' , 'gas' , 'plasma' )
> >> [ 'melt' , 'evaporate' , 'sublimate' , 'ionize' ]
Si vous avez suivi cette documentation depuis le début, vous remarquerez que get_triggers
renvoie en réalité plus de déclencheurs que ceux explicitement définis ci-dessus, comme to_liquid
et ainsi de suite. Celles-ci sont appelées auto-transitions
et seront présentées dans la section suivante.
En plus des transitions ajoutées explicitement, une méthode to_«state»()
est créée automatiquement chaque fois qu'un état est ajouté à une instance Machine
. Cette méthode passe à l’état cible quel que soit l’état dans lequel se trouve actuellement la machine :
lump . to_liquid ()
lump . state
> >> 'liquid'
lump . to_solid ()
lump . state
> >> 'solid'
Si vous le souhaitez, vous pouvez désactiver ce comportement en définissant auto_transitions=False
dans l'initialiseur Machine
.
Un déclencheur donné peut être attaché à plusieurs transitions, dont certaines peuvent potentiellement commencer ou se terminer dans le même état. Par exemple:
machine . add_transition ( 'transmogrify' , [ 'solid' , 'liquid' , 'gas' ], 'plasma' )
machine . add_transition ( 'transmogrify' , 'plasma' , 'solid' )
# This next transition will never execute
machine . add_transition ( 'transmogrify' , 'plasma' , 'gas' )
Dans ce cas, l'appel de transmogrify()
définira l'état du modèle sur 'solid'
s'il est actuellement 'plasma'
et le définira sur 'plasma'
sinon. (Notez que seule la première transition correspondante s'exécutera ; ainsi, la transition définie dans la dernière ligne ci-dessus ne fera rien.)
Vous pouvez également faire en sorte qu'un déclencheur provoque une transition de tous les états vers une destination particulière en utilisant le caractère générique '*'
:
machine . add_transition ( 'to_liquid' , '*' , 'liquid' )
Notez que les transitions génériques ne s'appliqueront qu'aux états qui existent au moment de l'appel add_transition(). L’appel d’une transition basée sur des caractères génériques lorsque le modèle est dans un état ajouté après la définition de la transition entraînera un message de transition non valide et n’entraînera pas de transition vers l’état cible.
Un déclencheur réflexif (déclencheur qui a le même état que la source et la destination) peut facilement être ajouté en spécifiant =
comme destination. Ceci est pratique si le même déclencheur réflexif doit être ajouté à plusieurs états. Par exemple:
machine . add_transition ( 'touch' , [ 'liquid' , 'gas' , 'plasma' ], '=' , after = 'change_shape' )
Cela ajoutera des transitions réflexives pour les trois états avec touch()
comme déclencheur et avec change_shape
exécuté après chaque déclencheur.
Contrairement aux transitions réflexives, les transitions internes ne quitteront jamais l’État. Cela signifie que les rappels liés à la transition, comme before
ou after
seront traités, tandis que les rappels liés à l'état, exit
ou enter
ne le seront pas. Pour définir une transition comme étant interne, définissez la destination sur None
.
machine . add_transition ( 'internal' , [ 'liquid' , 'gas' ], None , after = 'change_shape' )
Un souhait commun est que les transitions d’état suivent une séquence linéaire stricte. Par exemple, étant donné les états ['A', 'B', 'C']
, vous souhaiterez peut-être des transitions valides pour A
→ B
, B
→ C
et C
→ A
(mais pas d'autres paires).
Pour faciliter ce comportement, Transitions fournit une méthode add_ordered_transitions()
dans la classe Machine
:
states = [ 'A' , 'B' , 'C' ]
# See the "alternative initialization" section for an explanation of the 1st argument to init
machine = Machine ( states = states , initial = 'A' )
machine . add_ordered_transitions ()
machine . next_state ()
print ( machine . state )
> >> 'B'
# We can also define a different order of transitions
machine = Machine ( states = states , initial = 'A' )
machine . add_ordered_transitions ([ 'A' , 'C' , 'B' ])
machine . next_state ()
print ( machine . state )
> >> 'C'
# Conditions can be passed to 'add_ordered_transitions' as well
# If one condition is passed, it will be used for all transitions
machine = Machine ( states = states , initial = 'A' )
machine . add_ordered_transitions ( conditions = 'check' )
# If a list is passed, it must contain exactly as many elements as the
# machine contains states (A->B, ..., X->A)
machine = Machine ( states = states , initial = 'A' )
machine . add_ordered_transitions ( conditions = [ 'check_A2B' , ..., 'check_X2A' ])
# Conditions are always applied starting from the initial state
machine = Machine ( states = states , initial = 'B' )
machine . add_ordered_transitions ( conditions = [ 'check_B2C' , ..., 'check_A2B' ])
# With `loop=False`, the transition from the last state to the first state will be omitted (e.g. C->A)
# When you also pass conditions, you need to pass one condition less (len(states)-1)
machine = Machine ( states = states , initial = 'A' )
machine . add_ordered_transitions ( loop = False )
machine . next_state ()
machine . next_state ()
machine . next_state () # transitions.core.MachineError: "Can't trigger event next_state from state C!"
Le comportement par défaut dans Transitions consiste à traiter les événements instantanément. Cela signifie que les événements au sein d'une méthode on_enter
seront traités avant que les rappels liés à after
ne soient appelés.
def go_to_C ():
global machine
machine . to_C ()
def after_advance ():
print ( "I am in state B now!" )
def entering_C ():
print ( "I am in state C now!" )
states = [ 'A' , 'B' , 'C' ]
machine = Machine ( states = states , initial = 'A' )
# we want a message when state transition to B has been completed
machine . add_transition ( 'advance' , 'A' , 'B' , after = after_advance )
# call transition from state B to state C
machine . on_enter_B ( go_to_C )
# we also want a message when entering state C
machine . on_enter_C ( entering_C )
machine . advance ()
> >> 'I am in state C now!'
> >> 'I am in state B now!' # what?
L'ordre d'exécution de cet exemple est
prepare -> before -> on_enter_B -> on_enter_C -> after.
Si le traitement en file d'attente est activé, une transition sera terminée avant le déclenchement de la transition suivante :
machine = Machine ( states = states , queued = True , initial = 'A' )
...
machine . advance ()
> >> 'I am in state B now!'
> >> 'I am in state C now!' # That's better!
Cela se traduit par
prepare -> before -> on_enter_B -> queue(to_C) -> after -> on_enter_C.
Remarque importante : lors du traitement d'événements dans une file d'attente, l'appel déclencheur renverra toujours True
, car il n'existe aucun moyen de déterminer au moment de la mise en file d'attente si une transition impliquant des appels en file d'attente se terminera finalement avec succès. Cela est vrai même lorsqu'un seul événement est traité.
machine . add_transition ( 'jump' , 'A' , 'C' , conditions = 'will_fail' )
...
# queued=False
machine . jump ()
> >> False
# queued=True
machine . jump ()
> >> True
Lorsqu'un modèle est supprimé de la machine, transitions
suppriment également tous les événements associés de la file d'attente.
class Model :
def on_enter_B ( self ):
self . to_C () # add event to queue ...
self . machine . remove_model ( self ) # aaaand it's gone
Parfois, vous souhaitez qu'une transition particulière s'exécute uniquement si une condition spécifique se produit. Vous pouvez le faire en passant une méthode, ou une liste de méthodes, dans l'argument conditions
:
# Our Matter class, now with a bunch of methods that return booleans.
class Matter ( object ):
def is_flammable ( self ): return False
def is_really_hot ( self ): return True
machine . add_transition ( 'heat' , 'solid' , 'gas' , conditions = 'is_flammable' )
machine . add_transition ( 'heat' , 'solid' , 'liquid' , conditions = [ 'is_really_hot' ])
Dans l'exemple ci-dessus, appeler heat()
lorsque le modèle est à l'état 'solid'
passera à l'état 'gas'
si is_flammable
renvoie True
. Sinon, il passera à l'état 'liquid'
si is_really_hot
renvoie True
.
Pour plus de commodité, il existe également un argument 'unless'
qui se comporte exactement comme les conditions, mais inversé :
machine . add_transition ( 'heat' , 'solid' , 'gas' , unless = [ 'is_flammable' , 'is_really_hot' ])
Dans ce cas, le modèle passerait du solide au gaz chaque fois que heat()
se déclenche, à condition que is_flammable()
et is_really_hot()
renvoient False
.
Notez que les méthodes de vérification de condition recevront passivement des arguments facultatifs et/ou des objets de données transmis aux méthodes de déclenchement. Par exemple, l'appel suivant :
lump . heat ( temp = 74 )
# equivalent to lump.trigger('heat', temp=74)
... transmettrait le kwarg facultatif temp=74
à la vérification is_flammable()
(éventuellement encapsulée dans une instance EventData
). Pour en savoir plus à ce sujet, consultez la section Transmission de données ci-dessous.
Si vous voulez vous assurer qu'une transition est possible avant de la poursuivre, vous pouvez utiliser les fonctions may_<trigger_name>
qui ont été ajoutées à votre modèle. Votre modèle contient également la fonction may_trigger
pour vérifier un déclencheur par son nom :
# check if the current temperature is hot enough to trigger a transition
if lump . may_heat ():
# if lump.may_trigger("heat"):
lump . heat ()
Cela exécutera tous les rappels prepare
et évaluera les conditions attribuées aux transitions potentielles. Les contrôles de transition peuvent également être utilisés lorsque la destination d'une transition n'est pas (encore) disponible :
machine . add_transition ( 'elevate' , 'solid' , 'spiritual' )
assert not lump . may_elevate () # not ready yet :(
assert not lump . may_trigger ( "elevate" ) # same result for checks via trigger name
Vous pouvez joindre des rappels aux transitions ainsi qu'aux états. Chaque transition a des attributs 'before'
et 'after'
qui contiennent une liste de méthodes à appeler avant et après l'exécution de la transition :
class Matter ( object ):
def make_hissing_noises ( self ): print ( "HISSSSSSSSSSSSSSSS" )
def disappear ( self ): print ( "where'd all the liquid go?" )
transitions = [
{ 'trigger' : 'melt' , 'source' : 'solid' , 'dest' : 'liquid' , 'before' : 'make_hissing_noises' },
{ 'trigger' : 'evaporate' , 'source' : 'liquid' , 'dest' : 'gas' , 'after' : 'disappear' }
]
lump = Matter ()
machine = Machine ( lump , states , transitions = transitions , initial = 'solid' )
lump . melt ()
> >> "HISSSSSSSSSSSSSSSS"
lump . evaporate ()
> >> "where'd all the liquid go?"
Il existe également un rappel 'prepare'
qui est exécuté dès qu'une transition commence, avant que les 'conditions'
ne soient vérifiées ou que d'autres rappels ne soient exécutés.
class Matter ( object ):
heat = False
attempts = 0
def count_attempts ( self ): self . attempts += 1
def heat_up ( self ): self . heat = random . random () < 0.25
def stats ( self ): print ( 'It took you %i attempts to melt the lump!' % self . attempts )
@ property
def is_really_hot ( self ):
return self . heat
states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ]
transitions = [
{ 'trigger' : 'melt' , 'source' : 'solid' , 'dest' : 'liquid' , 'prepare' : [ 'heat_up' , 'count_attempts' ], 'conditions' : 'is_really_hot' , 'after' : 'stats' },
]
lump = Matter ()
machine = Machine ( lump , states , transitions = transitions , initial = 'solid' )
lump . melt ()
lump . melt ()
lump . melt ()
lump . melt ()
> >> "It took you 4 attempts to melt the lump!"
Notez que prepare
ne sera appelé que si l'état actuel est une source valide pour la transition nommée.
Les actions par défaut destinées à être exécutées avant ou après chaque transition peuvent être transmises à Machine
lors de l'initialisation avec before_state_change
et after_state_change
respectivement :
class Matter ( object ):
def make_hissing_noises ( self ): print ( "HISSSSSSSSSSSSSSSS" )
def disappear ( self ): print ( "where'd all the liquid go?" )
states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ]
lump = Matter ()
m = Machine ( lump , states , before_state_change = 'make_hissing_noises' , after_state_change = 'disappear' )
lump . to_gas ()
> >> "HISSSSSSSSSSSSSSSS"
> >> "where'd all the liquid go?"
Il existe également deux mots-clés pour les rappels qui doivent être exécutés indépendamment a) du nombre de transitions possibles, b) si une transition réussit et c) même si une erreur est générée lors de l'exécution d'un autre rappel. Les rappels transmis à Machine
avec prepare_event
seront exécutés une fois avant que le traitement des transitions possibles (et leurs rappels prepare
individuels) n'ait lieu. Les rappels de finalize_event
seront exécutés quel que soit le succès des transitions traitées. Notez que si une erreur se produit, elle sera attachée à event_data
en tant error
et pourra être récupérée avec send_event=True
.
from transitions import Machine
class Matter ( object ):
def raise_error ( self , event ): raise ValueError ( "Oh no" )
def prepare ( self , event ): print ( "I am ready!" )
def finalize ( self , event ): print ( "Result: " , type ( event . error ), event . error )
states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ]
lump = Matter ()
m = Machine ( lump , states , prepare_event = 'prepare' , before_state_change = 'raise_error' ,
finalize_event = 'finalize' , send_event = True )
try :
lump . to_gas ()
except ValueError :
pass
print ( lump . state )
# >>> I am ready!
# >>> Result: <class 'ValueError'> Oh no
# >>> initial
Parfois, les choses ne fonctionnent tout simplement pas comme prévu et nous devons gérer les exceptions et nettoyer le désordre pour que les choses continuent. Nous pouvons transmettre des rappels à on_exception
pour faire ceci :
from transitions import Machine
class Matter ( object ):
def raise_error ( self , event ): raise ValueError ( "Oh no" )
def handle_error ( self , event ):
print ( "Fixing things ..." )
del event . error # it did not happen if we cannot see it ...
states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ]
lump = Matter ()
m = Machine ( lump , states , before_state_change = 'raise_error' , on_exception = 'handle_error' , send_event = True )
try :
lump . to_gas ()
except ValueError :
pass
print ( lump . state )
# >>> Fixing things ...
# >>> initial
Comme vous l'avez probablement déjà réalisé, la manière standard de transmettre des éléments appelables aux états, conditions et transitions est par leur nom. Lors du traitement des rappels et des conditions, transitions
utiliseront leur nom pour récupérer l'appelable associé à partir du modèle. Si la méthode ne peut pas être récupérée et qu'elle contient des points, transitions
traiteront le nom comme un chemin vers une fonction de module et tenteront de l'importer. Vous pouvez également transmettre des noms de propriétés ou d'attributs. Ils seront encapsulés dans des fonctions mais ne pourront pas recevoir de données d'événement pour des raisons évidentes. Vous pouvez également transmettre directement des éléments appelables tels que des fonctions (liées). Comme mentionné précédemment, vous pouvez également transmettre des listes/tuples de noms d'appelables aux paramètres de rappel. Les rappels seront exécutés dans l’ordre dans lequel ils ont été ajoutés.
from transitions import Machine
from mod import imported_func
import random
class Model ( object ):
def a_callback ( self ):
imported_func ()
@ property
def a_property ( self ):
""" Basically a coin toss. """
return random . random () < 0.5
an_attribute = False
model = Model ()
machine = Machine ( model = model , states = [ 'A' ], initial = 'A' )
machine . add_transition ( 'by_name' , 'A' , 'A' , conditions = 'a_property' , after = 'a_callback' )
machine . add_transition ( 'by_reference' , 'A' , 'A' , unless = [ 'a_property' , 'an_attribute' ], after = model . a_callback )
machine . add_transition ( 'imported' , 'A' , 'A' , after = 'mod.imported_func' )
model . by_name ()
model . by_reference ()
model . imported ()
La résolution appelable se fait dans Machine.resolve_callable
. Cette méthode peut être remplacée si des stratégies de résolution appelables plus complexes sont nécessaires.
Exemple
class CustomMachine ( Machine ):
@ staticmethod
def resolve_callable ( func , event_data ):
# manipulate arguments here and return func, or super() if no manipulation is done.
super ( CustomMachine , CustomMachine ). resolve_callable ( func , event_data )
En résumé, il existe actuellement trois manières de déclencher des événements. Vous pouvez appeler les fonctions pratiques d'un modèle comme lump.melt()
, exécuter des déclencheurs par nom tels que lump.trigger("melt")
ou distribuer des événements sur plusieurs modèles avec machine.dispatch("melt")
(voir la section sur plusieurs modèles dans modèles d'initialisation alternatifs). Les rappels sur les transitions sont ensuite exécutés dans l'ordre suivant :
Rappel | État actuel | Commentaires |
---|---|---|
'machine.prepare_event' | source | exécuté une fois avant le traitement des transitions individuelles |
'transition.prepare' | source | exécuté dès le début de la transition |
'transition.conditions' | source | les conditions peuvent échouer et arrêter la transition |
'transition.unless' | source | les conditions peuvent échouer et arrêter la transition |
'machine.before_state_change' | source | rappels par défaut déclarés sur le modèle |
'transition.before' | source | |
'state.on_exit' | source | rappels déclarés sur l'état source |
<STATE CHANGE> | ||
'state.on_enter' | destination | rappels déclarés sur l'état de destination |
'transition.after' | destination | |
'machine.on_final' | destination | les rappels concernant les enfants seront appelés en premier |
'machine.after_state_change' | destination | rappels par défaut déclarés sur le modèle ; sera également appelé après les transitions internes |
'machine.on_exception' | source/destination | les rappels seront exécutés lorsqu'une exception a été levée |
'machine.finalize_event' | source/destination | les rappels seront exécutés même si aucune transition n'a eu lieu ou si une exception a été levée |
Si un rappel déclenche une exception, le traitement des rappels ne se poursuit pas. Cela signifie que lorsqu'une erreur se produit avant la transition (dans state.on_exit
ou avant), elle est interrompue. En cas d'augmentation après la transition (dans state.on_enter
ou version ultérieure), le changement d'état persiste et aucune restauration ne se produit. Les rappels spécifiés dans machine.finalize_event
seront toujours exécutés à moins que l'exception ne soit déclenchée par un rappel de finalisation lui-même. Notez que chaque séquence de rappel doit être terminée avant que l'étape suivante ne soit exécutée. Le blocage des rappels arrêtera l'ordre d'exécution et bloquera donc l'appel trigger
ou dispatch
lui-même. Si vous souhaitez que les rappels soient exécutés en parallèle, vous pouvez consulter les extensions AsyncMachine
pour le traitement asynchrone ou LockedMachine
pour le threading.
Parfois, vous devez transmettre aux fonctions de rappel enregistrées lors de l'initialisation de la machine des données qui reflètent l'état actuel du modèle. Transitions vous permet de le faire de deux manières différentes.
Tout d'abord (valeur par défaut), vous pouvez transmettre n'importe quel argument de position ou de mot-clé directement aux méthodes de déclenchement (créées lorsque vous appelez add_transition()
) :
class Matter ( object ):
def __init__ ( self ): self . set_environment ()
def set_environment ( self , temp = 0 , pressure = 101.325 ):
self . temp = temp
self . pressure = pressure
def print_temperature ( self ): print ( "Current temperature is %d degrees celsius." % self . temp )
def print_pressure ( self ): print ( "Current pressure is %.2f kPa." % self . pressure )
lump = Matter ()
machine = Machine ( lump , [ 'solid' , 'liquid' ], initial = 'solid' )
machine . add_transition ( 'melt' , 'solid' , 'liquid' , before = 'set_environment' )
lump . melt ( 45 ) # positional arg;
# equivalent to lump.trigger('melt', 45)
lump . print_temperature ()
> >> 'Current temperature is 45 degrees celsius.'
machine . set_state ( 'solid' ) # reset state so we can melt again
lump . melt ( pressure = 300.23 ) # keyword args also work
lump . print_pressure ()
> >> 'Current pressure is 300.23 kPa.'
Vous pouvez transmettre autant d'arguments que vous le souhaitez au déclencheur.
Il existe une limitation importante à cette approche : chaque fonction de rappel déclenchée par la transition d'état doit être capable de gérer tous les arguments. Cela peut poser des problèmes si les rappels attendent chacun des données quelque peu différentes.
Pour contourner ce problème, Transitions prend en charge une autre méthode d'envoi de données. Si vous définissez send_event=True
lors de l'initialisation Machine
, tous les arguments des déclencheurs seront encapsulés dans une instance EventData
et transmis à chaque rappel. (L'objet EventData
conserve également des références internes à l'état source, au modèle, à la transition, à la machine et au déclencheur associés à l'événement, au cas où vous auriez besoin d'y accéder pour quoi que ce soit.)
class Matter ( object ):
def __init__ ( self ):
self . temp = 0
self . pressure = 101.325
# Note that the sole argument is now the EventData instance.
# This object stores positional arguments passed to the trigger method in the
# .args property, and stores keywords arguments in the .kwargs dictionary.
def set_environment ( self , event ):
self . temp = event . kwargs . get ( 'temp' , 0 )
self . pressure = event . kwargs . get ( 'pressure' , 101.325 )
def print_pressure ( self ): print ( "Current pressure is %.2f kPa." % self . pressure )
lump = Matter ()
machine = Machine ( lump , [ 'solid' , 'liquid' ], send_event = True , initial = 'solid' )
machine . add_transition ( 'melt' , 'solid' , 'liquid' , before = 'set_environment' )
lump . melt ( temp = 45 , pressure = 1853.68 ) # keyword args
lump . print_pressure ()
> >> 'Current pressure is 1853.68 kPa.'
Dans tous les exemples jusqu'à présent, nous avons attaché une nouvelle instance Machine
à un modèle distinct ( lump
, une instance de la classe Matter
). Bien que cette séparation garde les choses en ordre (car vous n'avez pas besoin de patcher tout un tas de nouvelles méthodes dans la classe Matter
), elle peut également devenir ennuyeuse, car elle vous oblige à garder une trace des méthodes appelées sur la machine à états. , et lesquels sont appelés sur le modèle auquel la machine à états est liée (par exemple, lump.on_enter_StateA()
contre machine.add_transition()
).
Heureusement, Transitions est flexible et prend en charge deux autres modèles d'initialisation.
Tout d’abord, vous pouvez créer une machine à états autonome qui ne nécessite aucun autre modèle. Omettez simplement l'argument du modèle lors de l'initialisation :
machine = Machine ( states = states , transitions = transitions , initial = 'solid' )
machine . melt ()
machine . state
> >> 'liquid'
Si vous initialisez la machine de cette façon, vous pouvez ensuite attacher tous les événements déclencheurs (comme evaporate()
, sublimate()
, etc.) et toutes les fonctions de rappel directement à l'instance Machine
.
Cette approche présente l'avantage de consolider toutes les fonctionnalités de la machine à états en un seul endroit, mais peut sembler un peu contre nature si vous pensez que la logique d'état doit être contenue dans le modèle lui-même plutôt que dans un contrôleur séparé.
Une approche alternative (potentiellement meilleure) consiste à faire en sorte que le modèle hérite de la classe Machine
. Transitions est conçu pour prendre en charge l’héritage de manière transparente. (assurez-vous simplement de remplacer la méthode __init__
de la classe Machine
!) :
class Matter ( Machine ):
def say_hello ( self ): print ( "hello, new state!" )
def say_goodbye ( self ): print ( "goodbye, old state!" )
def __init__ ( self ):
states = [ 'solid' , 'liquid' , 'gas' ]
Machine . __init__ ( self , states = states , initial = 'solid' )
self . add_transition ( 'melt' , 'solid' , 'liquid' )
lump = Matter ()
lump . state
> >> 'solid'
lump . melt ()
lump . state
> >> 'liquid'
Ici, vous pouvez consolider toutes les fonctionnalités de la machine à états dans votre modèle existant, ce qui semble souvent plus naturel que de conserver toutes les fonctionnalités souhaitées dans une instance Machine
autonome distincte.
Une machine peut gérer plusieurs modèles qui peuvent être transmis sous forme de liste comme Machine(model=[model1, model2, ...])
. Dans les cas où vous souhaitez ajouter des modèles ainsi que l'instance de machine elle-même, vous pouvez transmettre l'espace réservé de variable de classe (chaîne) Machine.self_literal
lors de l'initialisation comme Machine(model=[Machine.self_literal, model1, ...])
. Vous pouvez également créer une machine autonome et enregistrer des modèles dynamiquement via machine.add_model
en passant model=None
au constructeur. De plus, vous pouvez utiliser machine.dispatch
pour déclencher des événements sur tous les modèles actuellement ajoutés. N'oubliez pas d'appeler machine.remove_model
si la machine est durable et que vos modèles sont temporaires et doivent être récupérés :
class Matter ():
pass
lump1 = Matter ()
lump2 = Matter ()
# setting 'model' to None or passing an empty list will initialize the machine without a model
machine = Machine ( model = None , states = states , transitions = transitions , initial = 'solid' )
machine . add_model ( lump1 )
machine . add_model ( lump2 , initial = 'liquid' )
lump1 . state
> >> 'solid'
lump2 . state
> >> 'liquid'
# custom events as well as auto transitions can be dispatched to all models
machine . dispatch ( "to_plasma" )
lump1 . state
> >> 'plasma'
assert lump1 . state == lump2 . state
machine . remove_model ([ lump1 , lump2 ])
del lump1 # lump1 is garbage collected
del lump2 # lump2 is garbage collected
Si vous ne fournissez pas d'état initial dans le constructeur de la machine à états, transitions
créeront et ajouteront un état par défaut appelé 'initial'
. Si vous ne souhaitez pas d'état initial par défaut, vous pouvez passer initial=None
. Cependant, dans ce cas, vous devez passer un état initial à chaque fois que vous ajoutez un modèle.
machine = Machine ( model = None , states = states , transitions = transitions , initial = None )
machine . add_model ( Matter ())
> >> "MachineError: No initial state configured for machine, must specify when adding model."
machine . add_model ( Matter (), initial = 'liquid' )
Les modèles avec plusieurs états peuvent attacher plusieurs machines en utilisant différentes valeurs model_attribute
. Comme mentionné dans Vérification de l'état, cela ajoutera des fonctions personnalisées is/to_<model_attribute>_<state_name>
:
lump = Matter ()
matter_machine = Machine ( lump , states = [ 'solid' , 'liquid' , 'gas' ], initial = 'solid' )
# add a second machine to the same model but assign a different state attribute
shipment_machine = Machine ( lump , states = [ 'delivered' , 'shipping' ], initial = 'delivered' , model_attribute = 'shipping_state' )
lump . state
> >> 'solid'
lump . is_solid () # check the default field
> >> True
lump . shipping_state
> >> 'delivered'
lump . is_shipping_state_delivered () # check the custom field.
> >> True
lump . to_shipping_state_shipping ()
> >> True
lump . is_shipping_state_delivered ()
> >> False
Les transitions incluent des fonctionnalités de journalisation très rudimentaires. Un certain nombre d'événements, à savoir les changements d'état, les déclencheurs de transition et les vérifications conditionnelles, sont enregistrés en tant qu'événements de niveau INFO à l'aide du module logging
Python standard. Cela signifie que vous pouvez facilement configurer la journalisation sur la sortie standard dans un script :
# Set up logging; The basic log level will be DEBUG
import logging
logging . basicConfig ( level = logging . DEBUG )
# Set transitions' log level to INFO; DEBUG messages will be omitted
logging . getLogger ( 'transitions' ). setLevel ( logging . INFO )
# Business as usual
machine = Machine ( states = states , transitions = transitions , initial = 'solid' )
...
Les machines sont décapables et peuvent être stockées et chargées de pickle
. Pour Python 3.3 et versions antérieures, dill
est requis.
import dill as pickle # only required for Python 3.3 and earlier
m = Machine ( states = [ 'A' , 'B' , 'C' ], initial = 'A' )
m . to_B ()
m . state
> >> B
# store the machine
dump = pickle . dumps ( m )
# load the Machine instance again
m2 = pickle . loads ( dump )
m2 . state
> >> B
m2 . states . keys ()
> >> [ 'A' , 'B' , 'C' ]
Comme vous l'avez probablement remarqué, transitions
utilisent certaines fonctionnalités dynamiques de Python pour vous offrir des moyens pratiques de gérer les modèles. Cependant, les vérificateurs de types statiques n'aiment pas que les attributs et les méthodes du modèle ne soient pas connus avant l'exécution. Historiquement, transitions
n'attribuaient pas non plus de méthodes pratiques déjà définies sur les modèles pour éviter les remplacements accidentels.
Mais ne vous inquiétez pas ! Vous pouvez utiliser le paramètre model_override
du constructeur de machine pour modifier la façon dont les modèles sont décorés. Si vous définissez model_override=True
, transitions
remplaceront uniquement les méthodes déjà définies. Cela empêche l'apparition de nouvelles méthodes au moment de l'exécution et vous permet également de définir les méthodes d'assistance que vous souhaitez utiliser.
from transitions import Machine
# Dynamic assignment
class Model :
pass
model = Model ()
default_machine = Machine ( model , states = [ "A" , "B" ], transitions = [[ "go" , "A" , "B" ]], initial = "A" )
print ( model . __dict__ . keys ()) # all convenience functions have been assigned
# >> dict_keys(['trigger', 'to_A', 'may_to_A', 'to_B', 'may_to_B', 'go', 'may_go', 'is_A', 'is_B', 'state'])
assert model . is_A () # Unresolved attribute reference 'is_A' for class 'Model'
# Predefined assigment: We are just interested in calling our 'go' event and will trigger the other events by name
class PredefinedModel :
# state (or another parameter if you set 'model_attribute') will be assigned anyway
# because we need to keep track of the model's state
state : str
def go ( self ) -> bool :
raise RuntimeError ( "Should be overridden!" )
def trigger ( self , trigger_name : str ) -> bool :
raise RuntimeError ( "Should be overridden!" )
model = PredefinedModel ()
override_machine = Machine ( model , states = [ "A" , "B" ], transitions = [[ "go" , "A" , "B" ]], initial = "A" , model_override = True )
print ( model . __dict__ . keys ())
# >> dict_keys(['trigger', 'go', 'state'])
model . trigger ( "to_B" )
assert model . state == "B"
Si vous souhaitez utiliser toutes les fonctions pratiques et ajouter quelques rappels, la définition d'un modèle peut devenir assez compliquée lorsque de nombreux états et transitions sont définis. La méthode generate_base_model
dans transitions
peut générer un modèle de base à partir d'une configuration de machine pour vous aider.
from transitions . experimental . utils import generate_base_model
simple_config = {
"states" : [ "A" , "B" ],
"transitions" : [
[ "go" , "A" , "B" ],
],
"initial" : "A" ,
"before_state_change" : "call_this" ,
"model_override" : True ,
}
class_definition = generate_base_model ( simple_config )
with open ( "base_model.py" , "w" ) as f :
f . write ( class_definition )
# ... in another file
from transitions import Machine
from base_model import BaseModel
class Model ( BaseModel ): # call_this will be an abstract method in BaseModel
def call_this ( self ) -> None :
# do something
model = Model ()
machine = Machine ( model , ** simple_config )
La définition des méthodes de modèle qui seront remplacées ajoute un peu de travail supplémentaire. Il peut s'avérer fastidieux de basculer entre les deux pour s'assurer que les noms d'événements sont correctement orthographiés, en particulier si les états et les transitions sont définis dans des listes avant ou après votre modèle. Vous pouvez réduire le passe-partout et l'incertitude liée au travail avec des chaînes en définissant les états sous forme d'énumérations. Vous pouvez également définir des transitions directement dans votre classe de modèle à l'aide de add_transitions
et event
. C'est à vous de décider si vous utilisez la fonction decorator add_transitions
ou event pour attribuer des valeurs aux attributs en fonction de votre style de code préféré. Ils fonctionnent tous les deux de la même manière, ont la même signature et devraient donner (presque) les mêmes indications de type IDE. Comme il s'agit toujours d'un travail en cours, vous devrez créer une classe Machine personnalisée et utiliser with_model_definitions pour les transitions afin de vérifier les transitions définies de cette façon.
from enum import Enum
from transitions . experimental . utils import with_model_definitions , event , add_transitions , transition
from transitions import Machine
class State ( Enum ):
A = "A"
B = "B"
C = "C"
class Model :
state : State = State . A
@ add_transitions ( transition ( source = State . A , dest = State . B ), [ State . C , State . A ])
@ add_transitions ({ "source" : State . B , "dest" : State . A })
def foo ( self ): ...
bar = event (
{ "source" : State . B , "dest" : State . A , "conditions" : lambda : False },
transition ( source = State . B , dest = State . C )
)
@ with_model_definitions # don't forget to define your model with this decorator!
class MyMachine ( Machine ):
pass
model = Model ()
machine = MyMachine ( model , states = State , initial = model . state )
model . foo ()
model . bar ()
assert model . state == State . C
model . foo ()
assert model . state == State . A
Même si le cœur des transitions reste léger, il existe une variété de MixIns pour étendre ses fonctionnalités. Actuellement pris en charge sont :
Il existe deux mécanismes pour récupérer une instance de machine d'état avec les fonctionnalités souhaitées activées. La première approche utilise la factory
de commodité avec les quatre paramètres graph
, nested
, locked
ou asyncio
définis sur True
si la fonctionnalité est requise :
from transitions . extensions import MachineFactory
# create a machine with mixins
diagram_cls = MachineFactory . get_predefined ( graph = True )
nested_locked_cls = MachineFactory . get_predefined ( nested = True , locked = True )
async_machine_cls = MachineFactory . get_predefined ( asyncio = True )
# create instances from these classes
# instances can be used like simple machines
machine1 = diagram_cls ( model , state , transitions )
machine2 = nested_locked_cls ( model , state , transitions )
Cette approche vise une utilisation expérimentale puisque dans ce cas les classes sous-jacentes n'ont pas besoin d'être connues. Cependant, les classes peuvent également être directement importées depuis transitions.extensions
. Le schéma de dénomination est le suivant :
Diagrammes | Imbriqué | Fermé | Asyncio | |
---|---|---|---|---|
Machine | ✘ | ✘ | ✘ | ✘ |
GraphMachine | ✓ | ✘ | ✘ | ✘ |
Machine hiérarchique | ✘ | ✓ | ✘ | ✘ |
Machine verrouillée | ✘ | ✘ | ✓ | ✘ |
MachineGraphiqueHiérarchique | ✓ | ✓ | ✘ | ✘ |
MachineGraphique Verrouillée | ✓ | ✘ | ✓ | ✘ |
Machine hiérarchique verrouillée | ✘ | ✓ | ✓ | ✘ |
LockedHierarchicalGraphMachine | ✓ | ✓ | ✓ | ✘ |
Machine asynchrone | ✘ | ✘ | ✘ | ✓ |
AsyncGraphMachine | ✓ | ✘ | ✘ | ✓ |
HiérarchiqueAsyncMachine | ✘ | ✓ | ✘ | ✓ |
HierarchicalAsyncGraphMachine | ✓ | ✓ | ✘ | ✓ |
Pour utiliser une machine à états riche en fonctionnalités, on pourrait écrire :
from transitions . extensions import LockedHierarchicalGraphMachine as LHGMachine
machine = LHGMachine ( model , states , transitions )
Transitions comprend un module d'extension qui permet l'imbrication des états. Cela nous permet de créer des contextes et de modéliser des cas où les états sont liés à certaines sous-tâches de la machine à états. Pour créer un état imbriqué, importez NestedState
à partir des transitions ou utilisez un dictionnaire avec les arguments d'initialisation name
et children
. Facultativement, initial
peut être utilisé pour définir un sous-état vers lequel transiter, lorsque l'état imbriqué est entré.
from transitions . extensions import HierarchicalMachine
states = [ 'standing' , 'walking' , { 'name' : 'caffeinated' , 'children' :[ 'dithering' , 'running' ]}]
transitions = [
[ 'walk' , 'standing' , 'walking' ],
[ 'stop' , 'walking' , 'standing' ],
[ 'drink' , '*' , 'caffeinated' ],
[ 'walk' , [ 'caffeinated' , 'caffeinated_dithering' ], 'caffeinated_running' ],
[ 'relax' , 'caffeinated' , 'standing' ]
]
machine = HierarchicalMachine ( states = states , transitions = transitions , initial = 'standing' , ignore_invalid_triggers = True )
machine . walk () # Walking now
machine . stop () # let's stop for a moment
machine . drink () # coffee time
machine . state
> >> 'caffeinated'
machine . walk () # we have to go faster
machine . state
> >> 'caffeinated_running'
machine . stop () # can't stop moving!
machine . state
> >> 'caffeinated_running'
machine . relax () # leave nested state
machine . state # phew, what a ride
> >> 'standing'
# machine.on_enter_caffeinated_running('callback_method')
Une configuration utilisant initial
pourrait ressembler à ceci :
# ...
states = [ 'standing' , 'walking' , { 'name' : 'caffeinated' , 'initial' : 'dithering' , 'children' : [ 'dithering' , 'running' ]}]
transitions = [
[ 'walk' , 'standing' , 'walking' ],
[ 'stop' , 'walking' , 'standing' ],
# this transition will end in 'caffeinated_dithering'...
[ 'drink' , '*' , 'caffeinated' ],
# ... that is why we do not need do specify 'caffeinated' here anymore
[ 'walk' , 'caffeinated_dithering' , 'caffeinated_running' ],
[ 'relax' , 'caffeinated' , 'standing' ]
]
# ...
Le mot-clé initial
du constructeur HierarchicalMachine
accepte les états imbriqués (par exemple initial='caffeinated_running'
) et une liste d'états qui est considéré comme un état parallèle (par exemple initial=['A', 'B']
) ou l'état actuel de un autre modèle ( initial=model.state
) qui devrait être effectivement l'une des options mentionnées précédemment. Notez que lors du passage d'une chaîne, transition
vérifiera l'état ciblé pour les sous-états initial
et l'utilisera comme état d'entrée. Cela se fera de manière récursive jusqu'à ce qu'un sous-état ne mentionne plus d'état initial. Les États parallèles ou un État présenté sous forme de liste seront utilisés « tels quels » et aucune autre évaluation initiale ne sera effectuée.
Notez que votre objet d'état créé précédemment doit être un NestedState
ou une classe dérivée de celui-ci. La classe State
standard utilisée dans les instances Machine
simples ne dispose pas des fonctionnalités requises pour l’imbrication.
from transitions . extensions . nesting import HierarchicalMachine , NestedState
from transitions import State
m = HierarchicalMachine ( states = [ 'A' ], initial = 'initial' )
m . add_state ( 'B' ) # fine
m . add_state ({ 'name' : 'C' }) # also fine
m . add_state ( NestedState ( 'D' )) # fine as well
m . add_state ( State ( 'E' )) # does not work!
Certaines choses à prendre en compte lorsque vous travaillez avec des états imbriqués : Les noms d'état sont concaténés avec NestedState.separator
. Actuellement, le séparateur est défini sur un trait de soulignement (« _ ») et se comporte donc de la même manière que la machine de base. Cela signifie qu'une bar
de sous-état de l'état foo
sera connue par foo_bar
. Un sous-état baz
de bar
sera appelé foo_bar_baz
et ainsi de suite. Lors de la saisie d'un sous-état, enter
sera appelé pour tous les états parents. Il en va de même pour les sous-états sortants. Troisièmement, les états imbriqués peuvent écraser le comportement de transition de leurs parents. Si une transition n'est pas connue de l'état actuel, elle sera déléguée à son parent.
Cela signifie que dans la configuration standard, les noms d'état dans les HSM NE DOIVENT PAS contenir de traits de soulignement. Pour transitions
il est impossible de dire si machine.add_state('state_name')
doit ajouter un état nommé state_name
ou ajouter un name
de sous-état à l' state
state . Dans certains cas, cela ne suffit cependant pas. Par exemple, si les noms d'état sont constitués de plus d'un mot et que vous souhaitez/devez utiliser un trait de soulignement pour les séparer au lieu de CamelCase
. Pour résoudre ce problème, vous pouvez modifier assez facilement le caractère utilisé pour la séparation. Vous pouvez même utiliser des caractères Unicode sophistiqués si vous utilisez Python 3. Définir le séparateur sur autre chose que le trait de soulignement modifie cependant certains comportements (transition automatique et définition des rappels) :
from transitions . extensions import HierarchicalMachine
from transitions . extensions . nesting import NestedState
NestedState . separator = '↦'
states = [ 'A' , 'B' ,
{ 'name' : 'C' , 'children' :[ '1' , '2' ,
{ 'name' : '3' , 'children' : [ 'a' , 'b' , 'c' ]}
]}
]
transitions = [
[ 'reset' , 'C' , 'A' ],
[ 'reset' , 'C↦2' , 'C' ] # overwriting parent reset
]
# we rely on auto transitions
machine = HierarchicalMachine ( states = states , transitions = transitions , initial = 'A' )
machine . to_B () # exit state A, enter state B
machine . to_C () # exit B, enter C
machine . to_C . s3 . a () # enter C↦a; enter C↦3↦a;
machine . state
> >> 'C↦3↦a'
assert machine . is_C . s3 . a ()
machine . to ( 'C↦2' ) # not interactive; exit C↦3↦a, exit C↦3, enter C↦2
machine . reset () # exit C↦2; reset C has been overwritten by C↦3
machine . state
> >> 'C'
machine . reset () # exit C, enter A
machine . state
> >> 'A'
# s.on_enter('C↦3↦a', 'callback_method')
Au lieu de to_C_3_a()
la transition automatique est appelée to_C.s3.a()
. Si votre sous-état commence par un chiffre, les transitions ajoutent un préfixe « s » (« 3 » devient « s3 ») au FunctionWrapper
de transition automatique pour se conformer au schéma de dénomination des attributs de Python. Si la complétion interactive n'est pas requise, to('C↦3↦a')
peut être appelée directement. De plus, on_enter/exit_<<state name>>
est remplacé par on_enter/exit(state_name, callback)
. Les contrôles d'État peuvent être effectués de la même manière. Au lieu de is_C_3_a()
, la variante FunctionWrapper
is_C.s3.a()
peut être utilisée.
Pour vérifier si l'état actuel est un sous-état d'un état spécifique, is_state
prend en charge le mot-clé allow_substates
:
machine . state
> >> 'C.2.a'
machine . is_C () # checks for specific states
> >> False
machine . is_C ( allow_substates = True )
> >> True
assert machine . is_C . s2 () is False
assert machine . is_C . s2 ( allow_substates = True ) # FunctionWrapper support allow_substate as well
Vous pouvez également utiliser des énumérations dans les HSM, mais gardez à l'esprit que Enum
sont comparées par valeur. Si vous avez une valeur plusieurs fois dans une arborescence d’états, ces états ne peuvent pas être distingués.
states = [ States . RED , States . YELLOW , { 'name' : States . GREEN , 'children' : [ 'tick' , 'tock' ]}]
states = [ 'A' , { 'name' : 'B' , 'children' : states , 'initial' : States . GREEN }, States . GREEN ]
machine = HierarchicalMachine ( states = states )
machine . to_B ()
machine . is_GREEN () # returns True even though the actual state is B_GREEN
HierarchicalMachine
a été réécrit à partir de zéro pour prendre en charge les états parallèles et une meilleure isolation des états imbriqués. Cela implique quelques ajustements basés sur les commentaires de la communauté. Pour avoir une idée de l'ordre de traitement et de la configuration, jetez un œil à l'exemple suivant :
from transitions . extensions . nesting import HierarchicalMachine
import logging
states = [ 'A' , 'B' , { 'name' : 'C' , 'parallel' : [{ 'name' : '1' , 'children' : [ 'a' , 'b' , 'c' ], 'initial' : 'a' ,
'transitions' : [[ 'go' , 'a' , 'b' ]]},
{ 'name' : '2' , 'children' : [ 'x' , 'y' , 'z' ], 'initial' : 'z' }],
'transitions' : [[ 'go' , '2_z' , '2_x' ]]}]
transitions = [[ 'reset' , 'C_1_b' , 'B' ]]
logging . basicConfig ( level = logging . INFO )
machine = HierarchicalMachine ( states = states , transitions = transitions , initial = 'A' )
machine . to_C ()
# INFO:transitions.extensions.nesting:Exited state A
# INFO:transitions.extensions.nesting:Entered state C
# INFO:transitions.extensions.nesting:Entered state C_1
# INFO:transitions.extensions.nesting:Entered state C_2
# INFO:transitions.extensions.nesting:Entered state C_1_a
# INFO:transitions.extensions.nesting:Entered state C_2_z
machine . go ()
# INFO:transitions.extensions.nesting:Exited state C_1_a
# INFO:transitions.extensions.nesting:Entered state C_1_b
# INFO:transitions.extensions.nesting:Exited state C_2_z
# INFO:transitions.extensions.nesting:Entered state C_2_x
machine . reset ()
# INFO:transitions.extensions.nesting:Exited state C_1_b
# INFO:transitions.extensions.nesting:Exited state C_2_x
# INFO:transitions.extensions.nesting:Exited state C_1
# INFO:transitions.extensions.nesting:Exited state C_2
# INFO:transitions.extensions.nesting:Exited state C
# INFO:transitions.extensions.nesting:Entered state B
Lorsque vous utilisez parallel
au lieu de children
, transitions
entreront dans tous les états de la liste passée en même temps. Le sous-état dans lequel entrer est défini par initial
qui doit toujours pointer vers un sous-état direct. Une nouvelle fonctionnalité consiste à définir des transitions locales en passant le mot-clé transitions
dans une définition d'état. La transition définie ci-dessus ['go', 'a', 'b']
n'est valable que dans C_1
. Bien que vous puissiez référencer des sous-états comme dans ['go', '2_z', '2_x']
vous ne pouvez pas référencer les états parents directement dans les transitions définies localement. Lorsqu’un état parent est quitté, ses enfants seront également quittés. En plus de l'ordre de traitement des transitions connu de Machine
où les transitions sont prises en compte dans l'ordre dans lequel elles ont été ajoutées, HierarchicalMachine
prend également en compte la hiérarchie. Les transitions définies dans les sous-étoiles seront évaluées en premier (par exemple C_1_a
se retrouvent avant C_2_z
) et les transitions définies avec le wildcard *
ne ajouteront (pour l'instant) que des transitions aux états racinaires (dans cet exemple A
, B
, C
) en commençant par 0,8,0 états imbriqués peut être ajouté directement et émettra la création d'états parents à la volée:
m = HierarchicalMachine ( states = [ 'A' ], initial = 'A' )
m . add_state ( 'B_1_a' )
m . to_B_1 ()
assert m . is_B ( allow_substates = True )
Expérimental dans 0.9.1: Vous pouvez utiliser des rappels on_final
soit dans les états ou sur le HSM lui-même. Les rappels seront déclenchés si a) L'état lui-même est tagué avec final
et vient d'être entré ou b) Tous les sous-états sont considérés comme finaux et au moins un sous-état vient d'entrer dans un état final. Dans le cas de B) tous les parents seront également considérés comme définitifs si la condition b) est vraie pour eux. Cela peut être utile dans les cas où le traitement se produit en parallèle et que votre HSM ou tout état parent doit être informé lorsque tous les sous-états ont atteint un état final:
from transitions . extensions import HierarchicalMachine
from functools import partial
# We initialize this parallel HSM in state A:
# / X
# / / yI
# A -> B - Y - yII [final]
# Z - zI
# zII [final]
def final_event_raised ( name ):
print ( "{} is final!" . format ( name ))
states = [ 'A' , { 'name' : 'B' , 'parallel' : [{ 'name' : 'X' , 'final' : True , 'on_final' : partial ( final_event_raised , 'X' )},
{ 'name' : 'Y' , 'transitions' : [[ 'final_Y' , 'yI' , 'yII' ]],
'initial' : 'yI' ,
'on_final' : partial ( final_event_raised , 'Y' ),
'states' :
[ 'yI' , { 'name' : 'yII' , 'final' : True }]
},
{ 'name' : 'Z' , 'transitions' : [[ 'final_Z' , 'zI' , 'zII' ]],
'initial' : 'zI' ,
'on_final' : partial ( final_event_raised , 'Z' ),
'states' :
[ 'zI' , { 'name' : 'zII' , 'final' : True }]
},
],
"on_final" : partial ( final_event_raised , 'B' )}]
machine = HierarchicalMachine ( states = states , on_final = partial ( final_event_raised , 'Machine' ), initial = 'A' )
# X will emit a final event right away
machine . to_B ()
# >>> X is final!
print ( machine . state )
# >>> ['B_X', 'B_Y_yI', 'B_Z_zI']
# Y's substate is final now and will trigger 'on_final' on Y
machine . final_Y ()
# >>> Y is final!
print ( machine . state )
# >>> ['B_X', 'B_Y_yII', 'B_Z_zI']
# Z's substate becomes final which also makes all children of B final and thus machine itself
machine . final_Z ()
# >>> Z is final!
# >>> B is final!
# >>> Machine is final!
Outre l'ordre sémantique, les états imbriqués sont très utiles si vous souhaitez spécifier des machines d'État pour des tâches spécifiques et prévoyez de les réutiliser. Avant 0,8.0 , une HierarchicalMachine
n'intégrerait pas l'instance de machine elle-même mais les états et les transitions en créant des copies d'entre eux. Cependant, puisque 0,8.0 (Nested)State
sont juste référencées, ce qui signifie que les modifications de la collection d'états et d'événements d'une machine d'une machine influenceront l'autre instance de la machine. Les modèles et leur état ne seront cependant pas partagés. Notez que les événements et les transitions sont également copiés par référence et seront partagés par les deux instances si vous n'utilisez pas le mot-clé remap
. Ce changement a été effectué pour être plus conforme à Machine
qui utilise également les instances State
passé par référence.
count_states = [ '1' , '2' , '3' , 'done' ]
count_trans = [
[ 'increase' , '1' , '2' ],
[ 'increase' , '2' , '3' ],
[ 'decrease' , '3' , '2' ],
[ 'decrease' , '2' , '1' ],
[ 'done' , '3' , 'done' ],
[ 'reset' , '*' , '1' ]
]
counter = HierarchicalMachine ( states = count_states , transitions = count_trans , initial = '1' )
counter . increase () # love my counter
states = [ 'waiting' , 'collecting' , { 'name' : 'counting' , 'children' : counter }]
transitions = [
[ 'collect' , '*' , 'collecting' ],
[ 'wait' , '*' , 'waiting' ],
[ 'count' , 'collecting' , 'counting' ]
]
collector = HierarchicalMachine ( states = states , transitions = transitions , initial = 'waiting' )
collector . collect () # collecting
collector . count () # let's see what we got; counting_1
collector . increase () # counting_2
collector . increase () # counting_3
collector . done () # collector.state == counting_done
collector . wait () # collector.state == waiting
Si une HierarchicalMachine
est transmise avec le mot-clé children
, l'état initial de cette machine sera affecté au nouvel état parent. Dans l'exemple ci-dessus, nous constatons que l'entrée counting
entrera également dans counting_1
. S'il s'agit d'un comportement indésirable et que la machine doit plutôt s'arrêter à l'état parent, l'utilisateur peut passer initial
comme False
comme {'name': 'counting', 'children': counter, 'initial': False}
.
Parfois, vous voulez qu'une telle collection d'État intégrée `` revienne '', ce qui signifie qu'après avoir été terminée, elle devrait sortir et transiter vers l'un de vos super états. Pour réaliser ce comportement, vous pouvez remapper les transitions d'état. Dans l'exemple ci-dessus, nous aimerions que le compteur revienne si l'État done
était atteint. Cela se fait comme suit:
states = [ 'waiting' , 'collecting' , { 'name' : 'counting' , 'children' : counter , 'remap' : { 'done' : 'waiting' }}]
... # same as above
collector . increase () # counting_3
collector . done ()
collector . state
> >> 'waiting' # be aware that 'counting_done' will be removed from the state machine
Comme mentionné ci-dessus, l'utilisation remap
copiera les événements et les transitions car ils ne peuvent pas être valides dans la machine d'état d'origine. Si une machine d'état réutilisée n'a pas d'état final, vous pouvez bien sûr ajouter les transitions manuellement. Si «compter» n'avait pas d'état «fait», nous pourrions simplement ajouter ['done', 'counter_3', 'waiting']
pour obtenir le même comportement.
Dans les cas où vous voulez que les états et les transitions soient copiés par valeur plutôt que par référence (par exemple, si vous souhaitez conserver le comportement pré-0,8), vous pouvez le faire en créant un NestedState
et en attribuant des copies profondes des événements et des états de la machine à il.
from transitions . extensions . nesting import NestedState
from copy import deepcopy
# ... configuring and creating counter
counting_state = NestedState ( name = "counting" , initial = '1' )
counting_state . states = deepcopy ( counter . states )
counting_state . events = deepcopy ( counter . events )
states = [ 'waiting' , 'collecting' , counting_state ]
Pour les machines d'état complexes, le partage de configurations plutôt que des machines instanciées pourrait être plus réalisable. D'autant plus que les machines instanciées doivent être dérivées de HierarchicalMachine
. Ces configurations peuvent être stockées et chargées facilement via JSON ou YAML (voir la FAQ). HierarchicalMachine
permet de définir des sous-états soit avec le children
clé ou states
. Si les deux sont présents, seuls children
seront pris en compte.
counter_conf = {
'name' : 'counting' ,
'states' : [ '1' , '2' , '3' , 'done' ],
'transitions' : [
[ 'increase' , '1' , '2' ],
[ 'increase' , '2' , '3' ],
[ 'decrease' , '3' , '2' ],
[ 'decrease' , '2' , '1' ],
[ 'done' , '3' , 'done' ],
[ 'reset' , '*' , '1' ]
],
'initial' : '1'
}
collector_conf = {
'name' : 'collector' ,
'states' : [ 'waiting' , 'collecting' , counter_conf ],
'transitions' : [
[ 'collect' , '*' , 'collecting' ],
[ 'wait' , '*' , 'waiting' ],
[ 'count' , 'collecting' , 'counting' ]
],
'initial' : 'waiting'
}
collector = HierarchicalMachine ( ** collector_conf )
collector . collect ()
collector . count ()
collector . increase ()
assert collector . is_counting_2 ()
Mots-clés supplémentaires:
title
(Facultatif): Définit le titre de l'image générée.show_conditions
(par défaut false): montre les conditions aux bords de transitionshow_auto_transitions
(par défaut false): affiche les transitions automobiles dans le graphiqueshow_state_attributes
(par défaut false): afficher des rappels (entrez, sortie), des balises et des délais de temps dans le graphiqueLes transitions peuvent générer des diagrammes d'état de base affichant toutes les transitions valides entre les états. Le support de base du diagramme génère une définition de la machine d'état de sirène qui peut être utilisée avec l'éditeur en direct de Mermaid, dans les fichiers Markdown dans Gitlab ou GitHub et d'autres services Web. Par exemple, ce code:
from transitions . extensions . diagrams import HierarchicalGraphMachine
import pyperclip
states = [ 'A' , 'B' , { 'name' : 'C' ,
'final' : True ,
'parallel' : [{ 'name' : '1' , 'children' : [ 'a' , { "name" : "b" , "final" : True }],
'initial' : 'a' ,
'transitions' : [[ 'go' , 'a' , 'b' ]]},
{ 'name' : '2' , 'children' : [ 'a' , { "name" : "b" , "final" : True }],
'initial' : 'a' ,
'transitions' : [[ 'go' , 'a' , 'b' ]]}]}]
transitions = [[ 'reset' , 'C' , 'A' ], [ "init" , "A" , "B" ], [ "do" , "B" , "C" ]]
m = HierarchicalGraphMachine ( states = states , transitions = transitions , initial = "A" , show_conditions = True ,
title = "Mermaid" , graph_engine = "mermaid" , auto_transitions = False )
m . init ()
pyperclip . copy ( m . get_graph (). draw ( None )) # using pyperclip for convenience
print ( "Graph copied to clipboard!" )
Produit ce diagramme (consultez la source de document pour voir la notation de marque):
---
Graphique de sirène
---
STEDATEAAGRAM-V2
Direction LR
Classdef S_Default Fill: blanc, couleur: noir
Classdef S_inactive Fill: blanc, couleur: noir
ClassDef S_Parallel Couleur: noir, remplissage: blanc
Classdef S_Active Couleur: rouge, remplissage: darksalmon
Classdef S_Previous Couleur: bleu, remplissage: Azure
indiquer "un" comme un
Classe A S_PREVIE
État "B" comme b
Classe B S_ACTIVE
affirmer "c" comme c
C -> [*]
Classe C S_Default
état c {
état "1" comme c_1
état c_1 {
[*] -> c_1_a
indiquer "A" comme c_1_a
état "b" comme c_1_b
C_1_b -> [*]
}
--
État "2" comme c_2
état c_2 {
[*] -> c_2_a
indiquer "A" comme c_2_a
État "b" comme c_2_b
C_2_b -> [*]
}
}
C -> a: réinitialiser
A -> b: init
B -> c: faire
C_1_a -> c_1_b: allez
C_2_a -> c_2_b: allez
[*] -> A
Pour utiliser des fonctionnalités graphiques plus sophistiquées, vous devrez installer graphviz
et / ou pygraphviz
. Pour générer des graphiques avec le package graphviz
, vous devez installer Graphviz manuellement ou via un gestionnaire de packages.
sudo apt-get install graphviz graphviz-dev # Ubuntu and Debian
brew install graphviz # MacOS
conda install graphviz python-graphviz # (Ana)conda
Vous pouvez maintenant installer les packages Python réels
pip install graphviz pygraphviz # install graphviz and/or pygraphviz manually...
pip install transitions[diagrams] # ... or install transitions with 'diagrams' extras which currently depends on pygraphviz
Actuellement, GraphMachine
utilisera pygraphviz
lorsqu'il est disponible et retombera à graphviz
lorsque pygraphviz
ne peut pas être trouvé. Si graphviz
n'est pas disponible non plus, mermaid
sera utilisé. Cela peut être remplacé en passant graph_engine="graphviz"
(ou "mermaid"
) au constructeur. Notez que cette valeur par défaut pourrait changer à l'avenir et le support pygraphviz
peut être supprimé. Avec Model.get_graph()
vous pouvez obtenir le graphique actuel ou la région d'intérêt (ROI) et le dessiner comme ceci:
# import transitions
from transitions . extensions import GraphMachine
m = Model ()
# without further arguments pygraphviz will be used
machine = GraphMachine ( model = m , ...)
# when you want to use graphviz explicitly
machine = GraphMachine ( model = m , graph_engine = "graphviz" , ...)
# in cases where auto transitions should be visible
machine = GraphMachine ( model = m , show_auto_transitions = True , ...)
# draw the whole graph ...
m . get_graph (). draw ( 'my_state_diagram.png' , prog = 'dot' )
# ... or just the region of interest
# (previous state, active state and all reachable states)
roi = m . get_graph ( show_roi = True ). draw ( 'my_state_diagram.png' , prog = 'dot' )
Cela produit quelque chose comme ceci:
Indépendamment du backend que vous utilisez, la fonction Draw accepte également un descripteur de fichiers ou un flux binaire comme premier argument. Si vous définissez ce paramètre sur None
, le flux d'octet sera retourné:
import io
with open ( 'a_graph.png' , 'bw' ) as f :
# you need to pass the format when you pass objects instead of filenames.
m . get_graph (). draw ( f , format = "png" , prog = 'dot' )
# you can pass a (binary) stream too
b = io . BytesIO ()
m . get_graph (). draw ( b , format = "png" , prog = 'dot' )
# or just handle the binary string yourself
result = m . get_graph (). draw ( None , format = "png" , prog = 'dot' )
assert result == b . getvalue ()
Les références et les partiels adoptés à mesure que les rappels seront résolus aussi bons que possible:
from transitions . extensions import GraphMachine
from functools import partial
class Model :
def clear_state ( self , deep = False , force = False ):
print ( "Clearing state ..." )
return True
model = Model ()
machine = GraphMachine ( model = model , states = [ 'A' , 'B' , 'C' ],
transitions = [
{ 'trigger' : 'clear' , 'source' : 'B' , 'dest' : 'A' , 'conditions' : model . clear_state },
{ 'trigger' : 'clear' , 'source' : 'C' , 'dest' : 'A' ,
'conditions' : partial ( model . clear_state , False , force = True )},
],
initial = 'A' , show_conditions = True )
model . get_graph (). draw ( 'my_state_diagram.png' , prog = 'dot' )
Cela devrait produire quelque chose de similaire à ceci:
Si le format des références ne convient pas à vos besoins, vous pouvez remplacer la méthode statique GraphMachine.format_references
. Si vous souhaitez sauter entièrement la référence, laissez GraphMachine.format_references
en None
. Jetez également un œil à notre exemple de carnets IPython / Jupyter pour un exemple plus détaillé sur la façon d'utiliser et de modifier les graphiques.
Dans les cas où la répartition des événements est effectuée dans les threads, on peut utiliser soit LockedMachine
ou LockedHierarchicalMachine
où l'accès à la fonction (! SIC) est sécurisé avec des verrous réentrants. Cela ne vous évite pas de corrompre votre machine en bricolant avec des variables de membres de votre modèle ou de votre machine d'état.
from transitions . extensions import LockedMachine
from threading import Thread
import time
states = [ 'A' , 'B' , 'C' ]
machine = LockedMachine ( states = states , initial = 'A' )
# let us assume that entering B will take some time
thread = Thread ( target = machine . to_B )
thread . start ()
time . sleep ( 0.01 ) # thread requires some time to start
machine . to_C () # synchronized access; won't execute before thread is done
# accessing attributes directly
thread = Thread ( target = machine . to_B )
thread . start ()
machine . new_attrib = 42 # not synchronized! will mess with execution order
Tout gestionnaire de contexte Python peut être transmis via l'argument de mot-clé machine_context
:
from transitions . extensions import LockedMachine
from threading import RLock
states = [ 'A' , 'B' , 'C' ]
lock1 = RLock ()
lock2 = RLock ()
machine = LockedMachine ( states = states , initial = 'A' , machine_context = [ lock1 , lock2 ])
Tous les contextes via machine_model
seront partagés entre tous les modèles enregistrés auprès de la Machine
. Des contextes par modèle peuvent également être ajoutés:
lock3 = RLock ()
machine . add_model ( model , model_context = lock3 )
Il est important que tous les gestionnaires de contexte fournis par l'utilisateur soient rentrés, car la machine d'état les appellera plusieurs fois, même dans le contexte d'une seule invocation de déclenchement.
Si vous utilisez Python 3.7 ou version ultérieure, vous pouvez utiliser AsyncMachine
pour travailler avec des rappels asynchrones. Vous pouvez mélanger des rappels synchrones et asynchrones si vous le souhaitez, mais cela peut avoir des effets secondaires indésirables. Notez que les événements doivent être attendus et que la boucle d'événement doit également être gérée par vous.
from transitions . extensions . asyncio import AsyncMachine
import asyncio
import time
class AsyncModel :
def prepare_model ( self ):
print ( "I am synchronous." )
self . start_time = time . time ()
async def before_change ( self ):
print ( "I am asynchronous and will block now for 100 milliseconds." )
await asyncio . sleep ( 0.1 )
print ( "I am done waiting." )
def sync_before_change ( self ):
print ( "I am synchronous and will block the event loop (what I probably shouldn't)" )
time . sleep ( 0.1 )
print ( "I am done waiting synchronously." )
def after_change ( self ):
print ( f"I am synchronous again. Execution took { int (( time . time () - self . start_time ) * 1000 ) } ms." )
transition = dict ( trigger = "start" , source = "Start" , dest = "Done" , prepare = "prepare_model" ,
before = [ "before_change" ] * 5 + [ "sync_before_change" ],
after = "after_change" ) # execute before function in asynchronously 5 times
model = AsyncModel ()
machine = AsyncMachine ( model , states = [ "Start" , "Done" ], transitions = [ transition ], initial = 'Start' )
asyncio . get_event_loop (). run_until_complete ( model . start ())
# >>> I am synchronous.
# I am asynchronous and will block now for 100 milliseconds.
# I am asynchronous and will block now for 100 milliseconds.
# I am asynchronous and will block now for 100 milliseconds.
# I am asynchronous and will block now for 100 milliseconds.
# I am asynchronous and will block now for 100 milliseconds.
# I am synchronous and will block the event loop (what I probably shouldn't)
# I am done waiting synchronously.
# I am done waiting.
# I am done waiting.
# I am done waiting.
# I am done waiting.
# I am done waiting.
# I am synchronous again. Execution took 101 ms.
assert model . is_Done ()
Alors, pourquoi avez-vous besoin d'utiliser Python 3.7 ou version ultérieure que vous pouvez demander. Le support asynchrone a été introduit plus tôt. AsyncMachine
utilise des contextvars
pour gérer les rappels de course lorsque de nouveaux événements arrivent avant la fin d'une transition:
async def await_never_return ():
await asyncio . sleep ( 100 )
raise ValueError ( "That took too long!" )
async def fix ():
await m2 . fix ()
m1 = AsyncMachine ( states = [ 'A' , 'B' , 'C' ], initial = 'A' , name = "m1" )
m2 = AsyncMachine ( states = [ 'A' , 'B' , 'C' ], initial = 'A' , name = "m2" )
m2 . add_transition ( trigger = 'go' , source = 'A' , dest = 'B' , before = await_never_return )
m2 . add_transition ( trigger = 'fix' , source = 'A' , dest = 'C' )
m1 . add_transition ( trigger = 'go' , source = 'A' , dest = 'B' , after = 'go' )
m1 . add_transition ( trigger = 'go' , source = 'B' , dest = 'C' , after = fix )
asyncio . get_event_loop (). run_until_complete ( asyncio . gather ( m2 . go (), m1 . go ()))
assert m1 . state == m2 . state
Cet exemple illustre en fait deux choses: premièrement, que «Go» appelé dans la transition de M1 de A
à être B
n'est pas annulé et deuxièmement, appeler m2.fix()
arrêtera la tentative de transition de M2 de A
à B
en exécutant «correctif» de A
à C
. Cette séparation ne serait pas possible sans contextvars
. Notez que prepare
et conditions
ne sont pas traitées comme des transitions en cours. Cela signifie qu'après que conditions
ont été évaluées, une transition est exécutée même si un autre événement s'est déjà produit. Les tâches ne seront annulées que lors de l'exécution en tant que rappel before
un rappel ou ultérieure.
AsyncMachine
dispose d'un mode de file d'attente spécial modèle qui peut être utilisé lorsque queued='model'
est transmise au constructeur. Avec une file d'attente spécifique au modèle, les événements ne seront en file d'attente que lorsqu'ils appartiennent au même modèle. En outre, une exception surélevée ne fera qu'effacer la file d'attente d'événements du modèle qui a soulevé cette exception. Par souci de simplicité, supposons que chaque événement dans asyncio.gather
ci-dessous n'est pas déclenché en même temps mais légèrement retardé:
asyncio . gather ( model1 . event1 (), model1 . event2 (), model2 . event1 ())
# execution order with AsyncMachine(queued=True)
# model1.event1 -> model1.event2 -> model2.event1
# execution order with AsyncMachine(queued='model')
# (model1.event1, model2.event1) -> model1.event2
asyncio . gather ( model1 . event1 (), model1 . error (), model1 . event3 (), model2 . event1 (), model2 . event2 (), model2 . event3 ())
# execution order with AsyncMachine(queued=True)
# model1.event1 -> model1.error
# execution order with AsyncMachine(queued='model')
# (model1.event1, model2.event1) -> (model1.error, model2.event2) -> model2.event3
Notez que les modes de file d'attente ne doivent pas être modifiés après la construction de la machine.
Si vos super-héros ont besoin d'un comportement personnalisé, vous pouvez jeter des fonctionnalités supplémentaires en décorant les états de la machine:
from time import sleep
from transitions import Machine
from transitions . extensions . states import add_state_features , Tags , Timeout
@ add_state_features ( Tags , Timeout )
class CustomStateMachine ( Machine ):
pass
class SocialSuperhero ( object ):
def __init__ ( self ):
self . entourage = 0
def on_enter_waiting ( self ):
self . entourage += 1
states = [{ 'name' : 'preparing' , 'tags' : [ 'home' , 'busy' ]},
{ 'name' : 'waiting' , 'timeout' : 1 , 'on_timeout' : 'go' },
{ 'name' : 'away' }] # The city needs us!
transitions = [[ 'done' , 'preparing' , 'waiting' ],
[ 'join' , 'waiting' , 'waiting' ], # Entering Waiting again will increase our entourage
[ 'go' , 'waiting' , 'away' ]] # Okay, let' move
hero = SocialSuperhero ()
machine = CustomStateMachine ( model = hero , states = states , transitions = transitions , initial = 'preparing' )
assert hero . state == 'preparing' # Preparing for the night shift
assert machine . get_state ( hero . state ). is_busy # We are at home and busy
hero . done ()
assert hero . state == 'waiting' # Waiting for fellow superheroes to join us
assert hero . entourage == 1 # It's just us so far
sleep ( 0.7 ) # Waiting...
hero . join () # Weeh, we got company
sleep ( 0.5 ) # Waiting...
hero . join () # Even more company o/
sleep ( 2 ) # Waiting...
assert hero . state == 'away' # Impatient superhero already left the building
assert machine . get_state ( hero . state ). is_home is False # Yupp, not at home anymore
assert hero . entourage == 3 # At least he is not alone
Actuellement, les transitions sont équipées des fonctionnalités d'état suivantes:
Timeout - déclenche un événement après un certain temps passé
timeout
(int, facultatif) - s'il est passé, un état entré sera le temps mort après les secondes timeout
on_timeout
(String / Callable, Facultatif) - sera appelé lorsque l'heure du délai d'attente a été atteinteAttributeError
lorsque timeout
est défini mais on_timeout
n'est pasTags - ajoute des balises aux états
tags
(liste, facultative) - attribue des balises à un étatState.is_<tag_name>
reviendra True
lorsque l'état aura été tagué avec tag_name
, sinon False
Erreur - soulève une MachineError
lorsqu'un état ne peut pas être laissé
Tags
(si vous utilisez Error
n'utilisez pas Tags
)accepted
(bool, facultatif) - marque un état comme acceptétags
de mot-clé peuvent être passées, contenant «accepté»auto_transitions
a été définie sur False
. Sinon, chaque État peut être sorti avec des méthodes to_<state>
.Volatile - initialise un objet chaque fois qu'un état est entré
volatile
(classe, facultatif) - Chaque fois que l'état est entré, un objet de classe de type sera affecté au modèle. Le nom d'attribut est défini par hook
. En cas d'omission, un volatileObject vide sera créé à la placehook
(String, Default = 'SCOPE') - Le nom d'attribut du modèle pour l'objet temporel. Vous pouvez écrire vos propres extensions State
et les ajouter de la même manière. Notez simplement que add_state_features
attend des mixins . Cela signifie que votre extension doit toujours appeler les méthodes remplacées __init__
, enter
et exit
. Votre extension peut hériter de l'État , mais fonctionnera également sans elle. L'utilisation de @add_state_features
a un inconvénient qui est que les machines décorées ne peuvent pas être marinées (plus précisément, le CustomState
généré dynamiquement ne peut pas être mariné). Cela pourrait être une raison d'écrire à la place une classe d'état personnalisée dédiée. Selon la machine d'état choisie, votre classe d'état personnalisée peut avoir besoin de fournir certaines fonctionnalités d'état. Par exemple, HierarchicalMachine
nécessite que votre état personnalisé soit une instance d' NestedState
( State
n'est pas suffisant). Pour injecter vos états, vous pouvez soit les affecter à state_cls
de classe de classe de votre Machine
, soit remplacer Machine.create_state
au cas où vous auriez besoin de procédures spécifiques effectuées chaque fois qu'un état est créé:
from transitions import Machine , State
class MyState ( State ):
pass
class CustomMachine ( Machine ):
# Use MyState as state class
state_cls = MyState
class VerboseMachine ( Machine ):
# `Machine._create_state` is a class method but we can
# override it to be an instance method
def _create_state ( self , * args , ** kwargs ):
print ( "Creating a new state with machine '{0}'" . format ( self . name ))
return MyState ( * args , ** kwargs )
Si vous souhaitez éviter entièrement les threads de votre AsyncMachine
, vous pouvez remplacer la fonction d'état Timeout
par AsyncTimeout
à partir de l'extension asyncio
:
import asyncio
from transitions . extensions . states import add_state_features
from transitions . extensions . asyncio import AsyncTimeout , AsyncMachine
@ add_state_features ( AsyncTimeout )
class TimeoutMachine ( AsyncMachine ):
pass
states = [ 'A' , { 'name' : 'B' , 'timeout' : 0.2 , 'on_timeout' : 'to_C' }, 'C' ]
m = TimeoutMachine ( states = states , initial = 'A' , queued = True ) # see remark below
asyncio . run ( asyncio . wait ([ m . to_B (), asyncio . sleep ( 0.1 )]))
assert m . is_B () # timeout shouldn't be triggered
asyncio . run ( asyncio . wait ([ m . to_B (), asyncio . sleep ( 0.3 )]))
assert m . is_C () # now timeout should have been processed
Vous devriez envisager de passer queued=True
au constructeur TimeoutMachine
. Cela s'assurera que les événements sont traités séquentiellement et éviteront les conditions de course asynchrones qui peuvent apparaître lorsque le délai et l'événement se produisent à proximité.
Vous pouvez jeter un œil à la FAQ pour une inspiration ou une vérification django-transitions
. Il a été développé par Christian Ledermann et est également hébergé sur Github. La documentation contient quelques exemples d'utilisation.
Premièrement, félicitations! Vous avez atteint la fin de la documentation! Si vous souhaitez essayer transitions
avant de l'installer, vous pouvez le faire dans un cahier Jupyter interactif sur mybinder.org. Cliquez simplement sur ce bouton.
Pour les rapports de bogues et d'autres problèmes, veuillez ouvrir un problème sur GitHub.
Pour les questions d'utilisation, publiez sur Stack Overflow, en vous assurant de marquer votre question avec la balise pytransitions
. N'oubliez pas de jeter un œil aux exemples prolongés!
Pour toute autre question, sollicitations ou grands cadeaux monétaires sans restriction, envoyez un courriel à Tal Yarkoni (auteur initial) et / ou Alexander Neumann (responsable actuel).