Eine schlanke, objektorientierte Zustandsmaschinenimplementierung in Python mit vielen Erweiterungen. Kompatibel mit Python 2.7+ und 3.0+.
pip install transitions
... oder das Repo von GitHub klonen und dann:
python setup.py install
Man sagt, ein gutes Beispiel sei mehr wert als 100 Seiten API-Dokumentation, eine Million Anweisungen oder tausend Worte.
Nun, „sie“ lügen wahrscheinlich ... aber hier ist trotzdem ein Beispiel:
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?" )
So, jetzt haben Sie eine Zustandsmaschine in NarcolepticSuperhero
integriert. Lasst uns ihn/sie/es auf eine Spritztour mitnehmen...
> >> 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
Während wir die Gedanken des tatsächlichen Batman nicht lesen können, können wir uns sicherlich den aktuellen Zustand unseres NarcolepticSuperhero
vorstellen.
Schauen Sie sich die Diagrams-Erweiterungen an, wenn Sie wissen möchten, wie das geht.
Eine Zustandsmaschine ist ein Verhaltensmodell , das aus einer endlichen Anzahl von Zuständen und Übergängen zwischen diesen Zuständen besteht. Innerhalb jedes Zustands und Übergangs können bestimmte Aktionen ausgeführt werden. Eine Zustandsmaschine muss in einem Anfangszustand starten. Bei Verwendung von transitions
kann eine Zustandsmaschine aus mehreren Objekten bestehen, wobei einige ( Maschinen ) Definitionen für die Manipulation anderer ( Modelle ) enthalten. Im Folgenden betrachten wir einige Kernkonzepte und wie man mit ihnen arbeitet.
Zustand . Ein Zustand repräsentiert eine bestimmte Bedingung oder Stufe in der Zustandsmaschine. Es handelt sich um eine bestimmte Verhaltensweise oder Phase in einem Prozess.
Übergang . Dies ist der Prozess oder das Ereignis, das dazu führt, dass die Zustandsmaschine von einem Zustand in einen anderen wechselt.
Modell . Die tatsächliche zustandsbehaftete Struktur. Es ist die Entität, die bei Übergängen aktualisiert wird. Es kann auch Aktionen definieren, die während Übergängen ausgeführt werden. Zum Beispiel direkt vor einem Übergang oder wenn ein Zustand betreten oder verlassen wird.
Maschine . Dies ist die Einheit, die das Modell, die Zustände, Übergänge und Aktionen verwaltet und steuert. Es ist der Dirigent, der den gesamten Prozess der Staatsmaschine orchestriert.
Auslösen . Dies ist das Ereignis, das einen Übergang initiiert, die Methode, die das Signal zum Starten eines Übergangs sendet.
Aktion . Spezifische Operation oder Aufgabe, die ausgeführt wird, wenn ein bestimmter Zustand betreten oder verlassen wird oder während eines Übergangs. Die Aktion wird durch Rückrufe implementiert, bei denen es sich um Funktionen handelt, die ausgeführt werden, wenn ein Ereignis eintritt.
Eine Zustandsmaschine zum Laufen zu bringen ist ziemlich einfach. Nehmen wir an, Sie haben das Objekt lump
(eine Instanz der Klasse Matter
) und möchten seine Zustände verwalten:
class Matter ( object ):
pass
lump = Matter ()
Sie können eine ( minimale ) funktionierende Zustandsmaschine, die an den lump
gebunden ist, wie folgt initialisieren:
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'
Eine Alternative besteht darin, ein Modell nicht explizit an den Machine
zu übergeben:
machine = Machine ( states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ], initial = 'solid' )
# The machine instance itself now acts as a model
machine . state
> >> 'solid'
Beachten Sie, dass ich dieses Mal das lump
nicht als Argument übergeben habe. Das erste an Machine
übergebene Argument fungiert als Modell. Wenn ich also dort etwas übergebe, werden dem Objekt alle praktischen Funktionen hinzugefügt. Wenn kein Modell bereitgestellt wird, fungiert die machine
selbst als Modell.
Als ich am Anfang „minimal“ sagte, lag das daran, dass dieser Zustandsautomat zwar technisch betriebsbereit ist, aber eigentlich nichts tut . Es beginnt im 'solid'
Zustand, wechselt aber nie in einen anderen Zustand, da noch keine Übergänge definiert sind!
Versuchen wir es noch einmal.
# 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'
Beachten Sie die glänzenden neuen Methoden, die an die Matter
-Instanz angehängt sind ( evaporate()
, ionize()
usw.). Jede Methode löst den entsprechenden Übergang aus. Übergänge können auch dynamisch ausgelöst werden, indem die Methode trigger()
aufgerufen wird, die mit dem Namen des Übergangs versehen ist, wie oben gezeigt. Mehr dazu im Abschnitt Auslösen eines Übergangs.
Die Seele jeder guten Staatsmaschine (und zweifellos auch vieler schlechter) ist eine Reihe von Zuständen. Oben haben wir die gültigen Modellzustände definiert, indem wir eine Liste von Zeichenfolgen an den Machine
übergeben haben. Aber intern werden Zustände tatsächlich als State
dargestellt.
Sie können Zustände auf verschiedene Arten initialisieren und ändern. Im Einzelnen können Sie:
Machine
, die die Namen der Zustände enthält, oderState
direkt initialisieren, oderDie folgenden Ausschnitte veranschaulichen verschiedene Möglichkeiten, um dasselbe Ziel zu erreichen:
# 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 ])
Zustände werden einmal initialisiert, wenn sie der Maschine hinzugefügt werden, und bleiben bestehen, bis sie von ihr entfernt werden. Mit anderen Worten: Wenn Sie die Attribute eines Statusobjekts ändern, wird diese Änderung beim nächsten Eintritt in diesen Status NICHT zurückgesetzt. Sehen Sie sich an, wie Sie Statusfunktionen erweitern können, falls Sie ein anderes Verhalten benötigen.
Aber nur Zustände zu haben und zwischen ihnen wechseln zu können (Übergänge), ist an sich nicht sehr nützlich. Was ist, wenn Sie etwas tun oder eine Aktion ausführen möchten, wenn Sie einen Zustand betreten oder verlassen? Hier kommen Rückrufe ins Spiel.
Einem State
kann auch eine Liste von enter
und exit
zugeordnet werden, die immer dann aufgerufen werden, wenn die Zustandsmaschine diesen Zustand betritt oder verlässt. Sie können Rückrufe während der Initialisierung angeben, indem Sie sie an einen State
Objektkonstruktor oder in einem State-Eigenschaftswörterbuch übergeben oder sie später hinzufügen.
Der Einfachheit halber werden bei jedem Hinzufügen eines neuen State
zu einer Machine
die Methoden on_enter_«state name»
und on_exit_«state name»
dynamisch auf der Maschine (nicht auf dem Modell!) erstellt, sodass Sie dynamisch neue Ein- und Ausgänge hinzufügen können Rückrufe später, wenn Sie sie benötigen.
# 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!'
Beachten Sie, dass der Rückruf on_enter_«state name»
nicht ausgelöst wird, wenn eine Maschine zum ersten Mal initialisiert wird. Wenn Sie beispielsweise einen on_enter_A()
-Rückruf definiert haben und die Machine
mit initial='A'
initialisieren, wird on_enter_A()
erst ausgelöst, wenn Sie das nächste Mal in den Status A
wechseln. (Wenn Sie sicherstellen müssen, dass on_enter_A()
bei der Initialisierung ausgelöst wird, können Sie einfach einen Dummy-Anfangszustand erstellen und dann to_A()
innerhalb der Methode __init__
explizit aufrufen.)
Neben der Übergabe von Rückrufen beim Initialisieren eines State
oder dem dynamischen Hinzufügen dieser Rückrufe ist es auch möglich, Rückrufe in der Modellklasse selbst zu definieren, was die Klarheit des Codes erhöhen kann. Zum Beispiel:
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' ])
Jetzt wird jedes Mal, wenn lump
in den Zustand A
übergeht, die in der Matter
-Klasse definierte Methode on_enter_A()
ausgelöst.
Sie können on_final
Rückrufe verwenden, die ausgelöst werden, wenn ein Status mit final=True
eingegeben wird.
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 ...
Sie können den aktuellen Status des Modells jederzeit überprüfen, indem Sie entweder:
.state
Attributs oderis_«state name»()
Und wenn Sie das tatsächliche State
für den aktuellen Status abrufen möchten, können Sie dies über die Methode get_state()
der Machine
tun.
lump . state
> >> 'solid'
lump . is_gas ()
> >> False
lump . is_solid ()
> >> True
machine . get_state ( lump . state ). name
> >> 'solid'
Wenn Sie möchten, können Sie Ihren eigenen Statusattributnamen auswählen, indem Sie beim Initialisieren der Machine
das Argument model_attribute
übergeben. Dadurch wird jedoch auch der Name von is_«state name»()
in is_«model_attribute»_«state name»()
geändert. In ähnlicher Weise werden automatische Übergänge to_«model_attribute»_«state name»()
anstelle von to_«state name»()
genannt. Dies geschieht, um mehreren Maschinen die Arbeit am selben Modell mit individuellen Statusattributnamen zu ermöglichen.
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
Bisher haben wir gesehen, wie wir Zustandsnamen vergeben und diese Namen für die Arbeit mit unserer Zustandsmaschine verwenden können. Wenn Sie eine strengere Eingabe und eine stärkere Vervollständigung des IDE-Codes bevorzugen (oder einfach nicht mehr „sesquipedalophobia“ eingeben können, weil Ihnen das Wort Angst macht), ist die Verwendung von Enumerationen möglicherweise das Richtige für Sie:
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
Sie können Aufzählungen und Zeichenfolgen kombinieren, wenn Sie möchten (z. B. [States.RED, 'ORANGE', States.YELLOW, States.GREEN]
), aber beachten Sie, dass transitions
intern Zustände weiterhin nach Namen behandeln ( enum.Enum.name
). Daher ist es nicht möglich, die Zustände 'GREEN'
und States.GREEN
gleichzeitig zu haben.
Einige der obigen Beispiele veranschaulichen bereits die Verwendung von Übergängen im Vorbeigehen, wir werden sie hier jedoch genauer untersuchen.
Wie bei Zuständen wird jeder Übergang intern als eigenes Objekt dargestellt – eine Instanz der Klasse Transition
. Der schnellste Weg, eine Reihe von Übergängen zu initialisieren, besteht darin, ein Wörterbuch oder eine Liste von Wörterbüchern an den Machine
zu übergeben. Das haben wir oben schon gesehen:
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 )
Das Definieren von Übergängen in Wörterbüchern hat den Vorteil der Klarheit, kann jedoch umständlich sein. Wenn Sie Wert auf Kürze legen, können Sie Übergänge mithilfe von Listen definieren. Stellen Sie einfach sicher, dass die Elemente in jeder Liste in derselben Reihenfolge sind wie die Positionsargumente in der Transition
(z. B. trigger
, source
, destination
usw.).
Die folgende Liste von Listen entspricht funktional der obigen Liste von Wörterbüchern:
transitions = [
[ 'melt' , 'solid' , 'liquid' ],
[ 'evaporate' , 'liquid' , 'gas' ],
[ 'sublimate' , 'solid' , 'gas' ],
[ 'ionize' , 'gas' , 'plasma' ]
]
Alternativ können Sie einer Machine
nach der Initialisierung Übergänge hinzufügen:
machine = Machine ( model = lump , states = states , initial = 'solid' )
machine . add_transition ( 'melt' , source = 'solid' , dest = 'liquid' )
Damit ein Übergang ausgeführt werden kann, muss er durch ein Ereignis ausgelöst werden. Hierzu gibt es zwei Möglichkeiten:
Verwendung der automatisch angehängten Methode im Basismodell:
> >> lump . melt ()
> >> lump . state
'liquid'
> >> lump . evaporate ()
> >> lump . state
'gas'
Beachten Sie, dass Sie diese Methoden nirgendwo explizit definieren müssen. Der Name jedes Übergangs ist an das Modell gebunden, das an den Machine
übergeben wird (in diesem Fall lump
). Dies bedeutet auch, dass Ihr Modell nicht bereits Methoden mit demselben Namen wie Ereignisauslöser enthalten sollte , da transitions
nur dann praktische Methoden an Ihr Modell anhängen, wenn der Platz noch nicht belegt ist. Wenn Sie dieses Verhalten ändern möchten, schauen Sie sich die FAQ an.
Mit der trigger
-Methode, jetzt an Ihr Modell angehängt (falls es noch nicht dort war). Mit dieser Methode können Sie Übergänge nach Namen ausführen, falls eine dynamische Auslösung erforderlich ist:
> >> lump . trigger ( 'melt' )
> >> lump . state
'liquid'
> >> lump . trigger ( 'evaporate' )
> >> lump . state
'gas'
Standardmäßig wird beim Auslösen eines ungültigen Übergangs eine Ausnahme ausgelöst:
> >> 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!"
Dieses Verhalten ist im Allgemeinen wünschenswert, da es Sie auf Probleme in Ihrem Code aufmerksam macht. In manchen Fällen möchten Sie jedoch möglicherweise ungültige Auslöser stillschweigend ignorieren. Sie können dies tun, indem Sie ignore_invalid_triggers=True
festlegen (entweder für jeden Bundesstaat einzeln oder global für alle Bundesstaaten):
> >> # 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 )
Wenn Sie wissen möchten, welche Übergänge ab einem bestimmten Zustand gültig sind, können Sie get_triggers
verwenden:
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' ]
Wenn Sie diese Dokumentation von Anfang an verfolgt haben, werden Sie feststellen, dass get_triggers
tatsächlich mehr Trigger zurückgibt als die oben gezeigten explizit definierten, wie z. B. to_liquid
und so weiter. Diese werden auto-transitions
genannt und im nächsten Abschnitt vorgestellt.
Zusätzlich zu allen explizit hinzugefügten Übergängen wird automatisch eine to_«state»()
Methode erstellt, wenn einer Machine
ein Status hinzugefügt wird. Diese Methode geht in den Zielzustand über, unabhängig davon, in welchem Zustand sich die Maschine gerade befindet:
lump . to_liquid ()
lump . state
> >> 'liquid'
lump . to_solid ()
lump . state
> >> 'solid'
Wenn Sie möchten, können Sie dieses Verhalten deaktivieren, indem Sie im Machine
auto_transitions=False
festlegen.
Ein bestimmter Auslöser kann an mehrere Übergänge angehängt werden, von denen einige möglicherweise im selben Zustand beginnen oder enden können. Zum Beispiel:
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' )
In diesem Fall wird durch den Aufruf von transmogrify()
der Zustand des Modells auf 'solid'
gesetzt, wenn es derzeit 'plasma'
ist, andernfalls auf 'plasma'
. (Beachten Sie, dass nur der erste passende Übergang ausgeführt wird; daher wird der in der letzten Zeile oben definierte Übergang nichts bewirken.)
Sie können einen Trigger auch dazu veranlassen, einen Übergang von allen Zuständen zu einem bestimmten Ziel auszulösen, indem Sie den Platzhalter '*'
verwenden:
machine . add_transition ( 'to_liquid' , '*' , 'liquid' )
Beachten Sie, dass Platzhalterübergänge nur für Zustände gelten, die zum Zeitpunkt des add_transition()-Aufrufs vorhanden sind. Der Aufruf eines auf Platzhaltern basierenden Übergangs, wenn sich das Modell in einem Zustand befindet, der nach der Definition des Übergangs hinzugefügt wurde, löst eine ungültige Übergangsmeldung aus und führt nicht zum Übergang in den Zielzustand.
Ein reflexiver Trigger (Trigger, der denselben Status wie Quelle und Ziel hat) kann einfach hinzugefügt werden, indem =
als Ziel angegeben wird. Dies ist praktisch, wenn derselbe reflexive Auslöser mehreren Zuständen hinzugefügt werden soll. Zum Beispiel:
machine . add_transition ( 'touch' , [ 'liquid' , 'gas' , 'plasma' ], '=' , after = 'change_shape' )
Dadurch werden reflexive Übergänge für alle drei Zustände mit touch()
als Auslöser und mit der Ausführung change_shape
nach jedem Auslöser hinzugefügt.
Im Gegensatz zu reflexiven Übergängen werden interne Übergänge den Zustand nie wirklich verlassen. Dies bedeutet, dass übergangsbezogene Rückrufe wie „ before
oder after
verarbeitet werden, während zustandsbezogene Rückrufe exit
oder enter
nicht verarbeitet werden. Um einen Übergang als intern zu definieren, legen Sie das Ziel auf None
fest.
machine . add_transition ( 'internal' , [ 'liquid' , 'gas' ], None , after = 'change_shape' )
Ein allgemeiner Wunsch besteht darin, dass Zustandsübergänge einer streng linearen Reihenfolge folgen. Beispielsweise möchten Sie bei gegebenen Zuständen ['A', 'B', 'C']
gültige Übergänge für A
→ B
, B
→ C
und C
→ A
(aber keine anderen Paare).
Um dieses Verhalten zu erleichtern, stellt Transitions eine add_ordered_transitions()
Methode in der Machine
-Klasse bereit:
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!"
Das Standardverhalten in Transitions besteht darin, Ereignisse sofort zu verarbeiten. Dies bedeutet, dass Ereignisse innerhalb einer on_enter
-Methode verarbeitet werden , bevor an after
gebundene Rückrufe aufgerufen werden.
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?
Die Ausführungsreihenfolge dieses Beispiels ist
prepare -> before -> on_enter_B -> on_enter_C -> after.
Wenn die Warteschlangenverarbeitung aktiviert ist, wird ein Übergang abgeschlossen, bevor der nächste Übergang ausgelöst wird:
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!
Daraus ergibt sich
prepare -> before -> on_enter_B -> queue(to_C) -> after -> on_enter_C.
Wichtiger Hinweis: Beim Verarbeiten von Ereignissen in einer Warteschlange gibt der Triggeraufruf immer True
zurück, da es zum Zeitpunkt der Warteschlange keine Möglichkeit gibt, zu bestimmen, ob ein Übergang mit Anrufen in der Warteschlange letztendlich erfolgreich abgeschlossen wird. Dies gilt auch dann, wenn nur ein einzelnes Ereignis verarbeitet wird.
machine . add_transition ( 'jump' , 'A' , 'C' , conditions = 'will_fail' )
...
# queued=False
machine . jump ()
> >> False
# queued=True
machine . jump ()
> >> True
Wenn ein Modell von der Maschine entfernt wird, werden transitions
auch alle zugehörigen Ereignisse aus der Warteschlange entfernt.
class Model :
def on_enter_B ( self ):
self . to_C () # add event to queue ...
self . machine . remove_model ( self ) # aaaand it's gone
Manchmal möchten Sie, dass ein bestimmter Übergang nur dann ausgeführt wird, wenn eine bestimmte Bedingung eintritt. Sie können dies tun, indem Sie eine Methode oder eine Liste von Methoden im Argument conditions
übergeben:
# 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' ])
Im obigen Beispiel wird beim Aufruf von heat()
, wenn sich das Modell im Zustand 'solid'
befindet, in den Zustand 'gas'
übergegangen, wenn is_flammable
True
zurückgibt. Andernfalls geht es in den Zustand 'liquid'
über, wenn is_really_hot
True
zurückgibt.
Der Einfachheit halber gibt es auch ein 'unless'
-Argument, das sich genau wie Bedingungen verhält, jedoch invertiert:
machine . add_transition ( 'heat' , 'solid' , 'gas' , unless = [ 'is_flammable' , 'is_really_hot' ])
In diesem Fall würde das Modell bei jedem Auslösen heat()
von fest zu gasförmig übergehen, vorausgesetzt, dass sowohl is_flammable()
als auch is_really_hot()
False
zurückgeben.
Beachten Sie, dass Methoden zur Bedingungsprüfung passiv optionale Argumente und/oder Datenobjekte empfangen, die an auslösende Methoden übergeben werden. Zum Beispiel der folgende Aufruf:
lump . heat ( temp = 74 )
# equivalent to lump.trigger('heat', temp=74)
... würde den optionalen kwarg temp=74
an die is_flammable()
-Prüfung übergeben (möglicherweise in eine EventData
Instanz eingeschlossen). Weitere Informationen hierzu finden Sie im Abschnitt „Übergabe von Daten“ weiter unten.
Wenn Sie sicherstellen möchten, dass ein Übergang möglich ist, bevor Sie mit ihm fortfahren, können Sie die Funktionen may_<trigger_name>
verwenden, die Ihrem Modell hinzugefügt wurden. Ihr Modell enthält auch die Funktion may_trigger
um einen Trigger anhand des Namens zu überprüfen:
# check if the current temperature is hot enough to trigger a transition
if lump . may_heat ():
# if lump.may_trigger("heat"):
lump . heat ()
Dadurch werden alle prepare
ausgeführt und die den potenziellen Übergängen zugewiesenen Bedingungen ausgewertet. Übergangsprüfungen können auch verwendet werden, wenn das Ziel eines Übergangs (noch) nicht verfügbar ist:
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
Sie können Rückrufe sowohl an Übergänge als auch an Zustände anhängen. Jeder Übergang verfügt über die Attribute 'before'
und 'after'
, die eine Liste von Methoden enthalten, die vor und nach der Ausführung des Übergangs aufgerufen werden sollen:
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?"
Es gibt auch einen 'prepare'
-Callback, der ausgeführt wird, sobald ein Übergang beginnt, bevor irgendwelche 'conditions'
überprüft oder andere Callbacks ausgeführt werden.
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!"
Beachten Sie, dass prepare
nicht aufgerufen wird, es sei denn, der aktuelle Status ist eine gültige Quelle für den benannten Übergang.
Standardaktionen, die vor oder nach jedem Übergang ausgeführt werden sollen, können während der Initialisierung mit before_state_change
bzw. after_state_change
an Machine
übergeben werden:
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?"
Es gibt auch zwei Schlüsselwörter für Rückrufe, die unabhängig davon ausgeführt werden sollten, a) wie viele Übergänge möglich sind, b) ob ein Übergang erfolgreich ist und c) selbst wenn während der Ausführung eines anderen Rückrufs ein Fehler auftritt. Rückrufe, die mit prepare_event
an Machine
übergeben werden, werden einmal ausgeführt, bevor die Verarbeitung möglicher Übergänge (und ihrer einzelnen prepare
-Rückrufe) erfolgt. Rückrufe von finalize_event
werden unabhängig vom Erfolg der verarbeiteten Übergänge ausgeführt. Beachten Sie, dass ein aufgetretener Fehler als error
an event_data
angehängt wird und mit send_event=True
abgerufen werden kann.
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
Manchmal laufen die Dinge einfach nicht wie beabsichtigt und wir müssen mit Ausnahmen umgehen und das Chaos beseitigen, um die Dinge am Laufen zu halten. Wir können Rückrufe an on_exception
übergeben, um dies zu tun:
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
Wie Sie wahrscheinlich bereits erkannt haben, erfolgt die Übergabe von Callables an Zustände, Bedingungen und Übergänge standardmäßig über den Namen. Bei der Verarbeitung von Rückrufen und Bedingungen verwenden transitions
ihren Namen, um das zugehörige Callable aus dem Modell abzurufen. Wenn die Methode nicht abgerufen werden kann und Punkte enthält, behandeln transitions
den Namen als Pfad zu einer Modulfunktion und versuchen, ihn zu importieren. Alternativ können Sie Namen von Eigenschaften oder Attributen übergeben. Sie werden in Funktionen verpackt, können aber aus offensichtlichen Gründen keine Ereignisdaten empfangen. Sie können Callables wie (gebundene) Funktionen auch direkt übergeben. Wie bereits erwähnt, können Sie auch Listen/Tupel von Callables-Namen an die Callback-Parameter übergeben. Rückrufe werden in der Reihenfolge ausgeführt, in der sie hinzugefügt wurden.
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 ()
Die aufrufbare Auflösung erfolgt in Machine.resolve_callable
. Diese Methode kann überschrieben werden, falls komplexere aufrufbare Auflösungsstrategien erforderlich sind.
Beispiel
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 )
Zusammenfassend gibt es derzeit drei Möglichkeiten, Ereignisse auszulösen. Sie können die Komfortfunktionen eines Modells wie lump.melt()
aufrufen, Trigger nach Namen wie lump.trigger("melt")
ausführen oder Ereignisse für mehrere Modelle mit machine.dispatch("melt")
auslösen (siehe Abschnitt über mehrere Modelle in alternative Initialisierungsmuster). Rückrufe bei Übergängen werden dann in der folgenden Reihenfolge ausgeführt:
Rückruf | Aktueller Stand | Kommentare |
---|---|---|
'machine.prepare_event' | source | einmal ausgeführt, bevor einzelne Transitionen verarbeitet werden |
'transition.prepare' | source | wird ausgeführt, sobald der Übergang beginnt |
'transition.conditions' | source | Bedingungen können fehlschlagen und den Übergang stoppen |
'transition.unless' | source | Bedingungen können fehlschlagen und den Übergang stoppen |
'machine.before_state_change' | source | Standardrückrufe, die für das Modell deklariert sind |
'transition.before' | source | |
'state.on_exit' | source | Rückrufe, die für den Quellstatus deklariert wurden |
<STATE CHANGE> | ||
'state.on_enter' | destination | Rückrufe, die für den Zielstatus deklariert wurden |
'transition.after' | destination | |
'machine.on_final' | destination | Rückrufe zu Kindern werden zuerst aufgerufen |
'machine.after_state_change' | destination | Standardrückrufe, die für das Modell deklariert wurden; wird auch nach internen Übergängen aufgerufen |
'machine.on_exception' | source/destination | Rückrufe werden ausgeführt, wenn eine Ausnahme ausgelöst wurde |
'machine.finalize_event' | source/destination | Rückrufe werden auch dann ausgeführt, wenn kein Übergang stattgefunden hat oder eine Ausnahme ausgelöst wurde |
Wenn ein Rückruf eine Ausnahme auslöst, wird die Verarbeitung der Rückrufe nicht fortgesetzt. Dies bedeutet, dass, wenn ein Fehler vor dem Übergang auftritt (in state.on_exit
oder früher), dieser angehalten wird. Falls es zu einer Erhöhung kommt, nachdem der Übergang durchgeführt wurde (in state.on_enter
oder später), bleibt die Statusänderung bestehen und es findet kein Rollback statt. In machine.finalize_event
angegebene Rückrufe werden immer ausgeführt, es sei denn, die Ausnahme wird durch einen finalisierenden Rückruf selbst ausgelöst. Beachten Sie, dass jede Rückrufsequenz abgeschlossen sein muss, bevor die nächste Stufe ausgeführt wird. Durch das Blockieren von Rückrufen wird die Ausführungsreihenfolge angehalten und somit der trigger
oder dispatch
Aufruf selbst blockiert. Wenn Sie möchten, dass Callbacks parallel ausgeführt werden, können Sie sich die Erweiterungen AsyncMachine
für asynchrone Verarbeitung oder LockedMachine
für Threading ansehen.
Manchmal müssen Sie den bei der Maschineninitialisierung registrierten Rückruffunktionen einige Daten übergeben, die den aktuellen Status des Modells widerspiegeln. Mit Transitions können Sie dies auf zwei verschiedene Arten tun.
Erstens (Standardeinstellung) können Sie beliebige Positions- oder Schlüsselwortargumente direkt an die Triggermethoden übergeben (erstellt, wenn Sie add_transition()
aufrufen):
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.'
Sie können dem Trigger beliebig viele Argumente übergeben.
Bei diesem Ansatz gibt es eine wichtige Einschränkung: Jede durch den Zustandsübergang ausgelöste Rückruffunktion muss in der Lage sein, alle Argumente zu verarbeiten. Dies kann zu Problemen führen, wenn die Rückrufe jeweils etwas unterschiedliche Daten erwarten.
Um dies zu umgehen, unterstützt Transitions eine alternative Methode zum Senden von Daten. Wenn Sie bei der Machine
send_event=True
festlegen, werden alle Argumente für die Trigger in eine EventData
-Instanz eingeschlossen und an jeden Rückruf weitergeleitet. (Das EventData
Objekt verwaltet auch interne Verweise auf den Quellstatus, das Modell, den Übergang, die Maschine und den Auslöser, die mit dem Ereignis verknüpft sind, falls Sie aus irgendeinem Grund darauf zugreifen müssen.)
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.'
In allen bisherigen Beispielen haben wir eine neue Machine
Instanz an ein separates Modell ( lump
, eine Instanz der Klasse Matter
) angehängt. Während diese Trennung für Ordnung sorgt (weil Sie nicht eine ganze Reihe neuer Methoden in die Matter
-Klasse einbauen müssen), kann sie auch lästig werden, da Sie den Überblick darüber behalten müssen, welche Methoden auf der Zustandsmaschine aufgerufen werden und welche auf dem Modell aufgerufen werden, an das die Zustandsmaschine gebunden ist (z. B. lump.on_enter_StateA()
vs. machine.add_transition()
).
Glücklicherweise ist Transitions flexibel und unterstützt zwei weitere Initialisierungsmuster.
Erstens können Sie eine eigenständige Zustandsmaschine erstellen, die überhaupt kein weiteres Modell erfordert. Lassen Sie das Modellargument während der Initialisierung einfach weg:
machine = Machine ( states = states , transitions = transitions , initial = 'solid' )
machine . melt ()
machine . state
> >> 'liquid'
Wenn Sie die Maschine auf diese Weise initialisieren, können Sie anschließend alle auslösenden Ereignisse (wie evaporate()
, sublimate()
usw.) und alle Rückruffunktionen direkt an die Machine
anhängen.
Dieser Ansatz hat den Vorteil, dass die gesamte Funktionalität der Zustandsmaschine an einem Ort konsolidiert wird, kann sich jedoch etwas unnatürlich anfühlen, wenn Sie der Meinung sind, dass die Zustandslogik im Modell selbst und nicht in einem separaten Controller enthalten sein sollte.
Ein alternativer (möglicherweise besserer) Ansatz besteht darin, das Modell von der Machine
erben zu lassen. Transitions ist darauf ausgelegt, die Vererbung nahtlos zu unterstützen. (Achten Sie nur darauf, die __init__
Methode der Klasse Machine
zu überschreiben!):
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'
Hier können Sie die gesamte Funktionalität der Zustandsmaschine in Ihrem vorhandenen Modell konsolidieren, was sich oft natürlicher anfühlt, als die gesamte gewünschte Funktionalität in einer separaten eigenständigen Machine
unterzubringen.
Eine Maschine kann mehrere Modelle verarbeiten, die als Liste wie Machine(model=[model1, model2, ...])
übergeben werden können. In Fällen, in denen Sie sowohl Modelle als auch die Maschineninstanz selbst hinzufügen möchten, können Sie den Klassenvariablenplatzhalter (String) Machine.self_literal
während der Initialisierung wie Machine(model=[Machine.self_literal, model1, ...])
übergeben. Sie können auch eine eigenständige Maschine erstellen und Modelle dynamisch über machine.add_model
registrieren, indem Sie model=None
an den Konstruktor übergeben. Darüber hinaus können Sie machine.dispatch
verwenden, um Ereignisse auf allen aktuell hinzugefügten Modellen auszulösen. Denken Sie daran, machine.remove_model
aufzurufen, wenn die Maschine langlebig ist und Ihre Modelle temporär sind und durch Müll gesammelt werden sollten:
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
Wenn Sie im Zustandsmaschinenkonstruktor keinen Anfangszustand angeben, erstellen und fügen transitions
einen Standardzustand namens 'initial'
hinzu. Wenn Sie keinen Standardanfangszustand wünschen, können Sie initial=None
übergeben. Allerdings müssen Sie in diesem Fall jedes Mal, wenn Sie ein Modell hinzufügen, einen Anfangszustand übergeben.
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' )
Modelle mit mehreren Zuständen könnten mehrere Maschinen mit unterschiedlichen model_attribute
Werten anhängen. Wie unter Status prüfen erwähnt, werden dadurch benutzerdefinierte is/to_<model_attribute>_<state_name>
-Funktionen hinzugefügt:
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
Transitions umfasst sehr rudimentäre Protokollierungsfunktionen. Eine Reihe von Ereignissen – nämlich Zustandsänderungen, Übergangsauslöser und Bedingungsprüfungen – werden mithilfe des standardmäßigen Python- logging
als Ereignisse auf INFO-Ebene protokolliert. Das bedeutet, dass Sie die Protokollierung für die Standardausgabe einfach in einem Skript konfigurieren können:
# 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' )
...
Maschinen sind beizbar und können mit pickle
gelagert und beladen werden. Für Python 3.3 und früher ist dill
erforderlich.
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' ]
Wie Sie wahrscheinlich bemerkt haben, nutzt transitions
einige der dynamischen Funktionen von Python, um Ihnen praktische Möglichkeiten für den Umgang mit Modellen zu bieten. Statische Typprüfer mögen es jedoch nicht, wenn Modellattribute und -methoden vor der Laufzeit nicht bekannt sind. In der Vergangenheit wurden transitions
auch keine bereits in Modellen definierten Komfortmethoden zugewiesen, um versehentliche Überschreibungen zu verhindern.
Aber keine Sorge! Mit dem Maschinenkonstruktorparameter model_override
können Sie ändern, wie Modelle dekoriert werden. Wenn Sie model_override=True
festlegen, überschreiben transitions
nur bereits definierte Methoden. Dadurch wird verhindert, dass zur Laufzeit neue Methoden angezeigt werden, und Sie können außerdem festlegen, welche Hilfsmethoden Sie verwenden möchten.
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"
Wenn Sie alle praktischen Funktionen nutzen und einige Rückrufe hinzufügen möchten, kann die Definition eines Modells ziemlich kompliziert werden, wenn Sie viele Zustände und Übergänge definiert haben. Die Methode generate_base_model
in transitions
kann ein Basismodell aus einer Maschinenkonfiguration generieren, um Ihnen dabei zu helfen.
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 )
Das Definieren von Modellmethoden, die überschrieben werden, erfordert etwas zusätzlichen Aufwand. Es kann umständlich sein, hin und her zu wechseln, um sicherzustellen, dass Ereignisnamen richtig geschrieben sind, insbesondere wenn Zustände und Übergänge in Listen vor oder nach Ihrem Modell definiert sind. Sie können den Boilerplate und die Unsicherheit bei der Arbeit mit Zeichenfolgen reduzieren, indem Sie Zustände als Aufzählungen definieren. Mithilfe von add_transitions
und event
können Sie Übergänge auch direkt in Ihrer Modellklasse definieren. Es liegt an Ihnen, ob Sie den Funktionsdekorator add_transitions
oder event verwenden, um Attributen Werte zuzuweisen, abhängig von Ihrem bevorzugten Codestil. Beide funktionieren auf die gleiche Weise, haben die gleiche Signatur und sollten zu (fast) den gleichen Hinweisen vom IDE-Typ führen. Da dies noch in Arbeit ist, müssen Sie eine benutzerdefinierte Maschinenklasse erstellen und „with_model_definitions“ für Übergänge verwenden, um nach so definierten Übergängen zu suchen.
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
Auch wenn der Kern der Übergänge kompakt gehalten ist, gibt es eine Vielzahl von MixIns, um die Funktionalität zu erweitern. Derzeit unterstützt werden:
Es gibt zwei Mechanismen zum Abrufen einer Zustandsmaschineninstanz mit aktivierten gewünschten Funktionen. Der erste Ansatz nutzt die Convenience factory
wobei die vier Parameter graph
, nested
, locked
oder asyncio
auf True
gesetzt sind, wenn die Funktion erforderlich ist:
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 )
Dieser Ansatz zielt auf den experimentellen Einsatz ab, da in diesem Fall die zugrunde liegenden Klassen nicht bekannt sein müssen. Allerdings können Klassen auch direkt aus transitions.extensions
importiert werden. Das Namensschema ist wie folgt:
Diagramme | Verschachtelt | Gesperrt | Asyncio | |
---|---|---|---|---|
Maschine | ✘ | ✘ | ✘ | ✘ |
GraphMachine | ✓ | ✘ | ✘ | ✘ |
Hierarchische Maschine | ✘ | ✓ | ✘ | ✘ |
Gesperrte Maschine | ✘ | ✘ | ✓ | ✘ |
HierarchicalGraphMachine | ✓ | ✓ | ✘ | ✘ |
LockedGraphMachine | ✓ | ✘ | ✓ | ✘ |
LockedHierarchicalMachine | ✘ | ✓ | ✓ | ✘ |
GesperrtHierarchicalGraphMachine | ✓ | ✓ | ✓ | ✘ |
AsyncMachine | ✘ | ✘ | ✘ | ✓ |
AsyncGraphMachine | ✓ | ✘ | ✘ | ✓ |
HierarchicalAsyncMachine | ✘ | ✓ | ✘ | ✓ |
HierarchicalAsyncGraphMachine | ✓ | ✓ | ✘ | ✓ |
Um eine funktionsreiche Zustandsmaschine zu verwenden, könnte man schreiben:
from transitions . extensions import LockedHierarchicalGraphMachine as LHGMachine
machine = LHGMachine ( model , states , transitions )
Transitions enthält ein Erweiterungsmodul, das das Verschachteln von Zuständen ermöglicht. Dies ermöglicht es uns, Kontexte zu erstellen und Fälle zu modellieren, in denen Zustände mit bestimmten Unteraufgaben in der Zustandsmaschine verknüpft sind. Um einen verschachtelten Zustand zu erstellen, importieren Sie entweder NestedState
aus Übergängen oder verwenden Sie ein Wörterbuch mit den Initialisierungsargumenten name
und children
. Optional kann initial
verwendet werden, um einen Unterzustand zu definieren, zu dem beim Eintritt in den verschachtelten Zustand übergegangen werden soll.
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')
Eine Konfiguration, die initial
verwendet, könnte wie folgt aussehen:
# ...
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' ]
]
# ...
Das Schlüsselwort initial
des HierarchicalMachine
-Konstruktors akzeptiert verschachtelte Zustände (z. B. initial='caffeinated_running'
) und eine Liste von Zuständen, die als paralleler Zustand (z. B. initial=['A', 'B']
) oder der aktuelle Zustand von betrachtet werden ein anderes Modell ( initial=model.state
), das effektiv eine der zuvor genannten Optionen sein sollte. Beachten Sie, dass der transition
beim Übergeben einer Zeichenfolge den Zielzustand auf initial
Unterzustände überprüft und diesen als Eintrittszustand verwendet. Dies wird rekursiv durchgeführt, bis ein Unterzustand keinen Anfangszustand mehr erwähnt. Parallele Zustände oder ein als Liste übergebener Zustand werden unverändert verwendet und es wird keine weitere Erstbewertung durchgeführt.
Beachten Sie, dass Ihr zuvor erstelltes Statusobjekt ein NestedState
oder eine davon abgeleitete Klasse sein muss . Der in einfachen Machine
verwendeten Standard- State
Klasse fehlen die für die Verschachtelung erforderlichen Funktionen.
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!
Einige Dinge, die beim Arbeiten mit verschachtelten Zuständen berücksichtigt werden müssen: Zustandsnamen werden mit NestedState.separator
verkettet . Derzeit ist das Trennzeichen auf Unterstrich ('_') eingestellt und verhält sich daher ähnlich wie die Basismaschine. Dies bedeutet, dass foo_bar
eine bar
aus dem Zustand foo
kennt. Ein Substate- baz
von bar
wird als foo_bar_baz
usw. bezeichnet. Bei der Eingabe eines Unterzustandes wird enter
für alle übergeordneten Zustände aufgerufen. Das Gleiche gilt für das Verlassen von Unterstaaten. Drittens können verschachtelte Zustände das Übergangsverhalten ihrer Eltern überschreiben. Wenn ein Übergang dem aktuellen Status nicht bekannt ist, wird er an seinen übergeordneten Status delegiert.
Das bedeutet, dass in der Standardkonfiguration Zustandsnamen in HSMs KEINE Unterstriche enthalten DÜRFEN. Bei transitions
ist es unmöglich zu sagen, ob machine.add_state('state_name')
einen Zustand namens state_name
oder einen name
zum Zustand state
hinzufügen soll. In manchen Fällen reicht dies jedoch nicht aus. Zum Beispiel, wenn Staatsnamen aus mehr als einem Wort bestehen und Sie anstelle von CamelCase
einen Unterstrich verwenden möchten/müssen, um sie zu trennen. Um dieses Problem zu lösen, können Sie das zur Trennung verwendete Zeichen ganz einfach ändern. Sie können sogar ausgefallene Unicode-Zeichen verwenden, wenn Sie Python 3 verwenden. Wenn Sie das Trennzeichen auf etwas anderes als den Unterstrich setzen, ändern sich jedoch einige Verhaltensweisen (auto_transition und Festlegen von Rückrufen):
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')
Anstelle von to_C_3_a()
wird der automatische Übergang als to_C.s3.a()
bezeichnet. Wenn Ihr Unterzustand mit einer Ziffer beginnt, fügt Transitionen dem automatischen Übergangs- FunctionWrapper
ein Präfix „s“ („3“ wird zu „s3“) hinzu, um dem Attributbenennungsschema von Python zu entsprechen. Wenn keine interaktive Vervollständigung erforderlich ist, kann to('C↦3↦a')
direkt aufgerufen werden. Darüber hinaus wird on_enter/exit_<<state name>>
durch on_enter/exit(state_name, callback)
ersetzt. Staatliche Kontrollen können auf ähnliche Weise durchgeführt werden. Anstelle von is_C_3_a()
kann auch die FunctionWrapper
Variante is_C.s3.a()
verwendet werden.
Um zu überprüfen, ob der aktuelle Zustand ein Unterzustand eines bestimmten Zustands ist, unterstützt is_state
das 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
Sie können Aufzählungen auch in HSMs verwenden. Beachten Sie jedoch, dass Enum
nach Wert verglichen werden. Wenn ein Wert mehr als einmal in einem Zustandsbaum vorhanden ist, können diese Zustände nicht unterschieden werden.
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
wurde von Grund auf neu geschrieben, um parallele Zustände und eine bessere Isolierung verschachtelter Zustände zu unterstützen. Dies erfordert einige Optimierungen, die auf dem Feedback der Community basieren. Um eine Vorstellung von der Verarbeitungsreihenfolge und Konfiguration zu bekommen, sehen Sie sich das folgende Beispiel an:
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
Bei Verwendung parallel
anstelle von children
treten transitions
gleichzeitig in alle Zustände der übergebenen Liste ein. Welcher Unterzustand eingegeben werden soll, wird durch initial
definiert, der immer auf einen direkten Unterzustand verweisen sollte. Eine neue Funktion besteht darin, lokale Übergänge zu definieren, indem das Schlüsselwort transitions
in einer Zustandsdefinition übergeben wird. Der oben definierte Übergang ['go', 'a', 'b']
ist nur in C_1
gültig. Während Sie wie in ['go', '2_z', '2_x']
auf Unterzustände verweisen können, können Sie in lokal definierten Übergängen nicht direkt auf übergeordnete Zustände verweisen. Wenn ein übergeordneter Zustand verlassen wird, werden auch seine untergeordneten Staaten verlassen. Zusätzlich zur von Machine
bekannten Verarbeitungsreihenfolge von Übergängen, bei der Übergänge in der Reihenfolge berücksichtigt werden, in der sie hinzugefügt wurden, berücksichtigt HierarchicalMachine
auch die Hierarchie. In Substates definierte Übergänge werden zuerst bewertet (z. A
C_1_a
bleibt vor C_2_z
) B
Übergänge , die mit C
*
definiert werden. kann direkt hinzugefügt werden und wird die Erstellung von übergeordneten Zuständen im Laufe der Fliege ausstellen:
m = HierarchicalMachine ( states = [ 'A' ], initial = 'A' )
m . add_state ( 'B_1_a' )
m . to_B_1 ()
assert m . is_B ( allow_substates = True )
Experimentell in 0.9.1: Sie können on_final
-Rückrufe entweder in Zuständen oder auf dem HSM selbst verwenden. Rückrufe werden ausgelöst, wenn a) der Staat selbst mit final
markiert ist und gerade eingegeben wurde oder b) alle Substationen als endgültig angesehen werden und mindestens ein Untergang in einen endgültigen Zustand eingetreten ist. Im Falle von b) werden alle Eltern auch als endgültig angesehen, wenn die Bedingung b) für sie zutrifft. Dies kann in Fällen nützlich sein, in denen die Verarbeitung parallel und Ihr HSM- oder ein übergeordneter Zustand benachrichtigt werden sollte, wenn alle Substantes einen endgültigen Zustand erreicht haben:
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!
Neben der semantischen Ordnung sind verschachtelte Staaten sehr praktisch, wenn Sie Staatsmaschinen für bestimmte Aufgaben angeben und sie wiederverwenden möchten. Vor 0,8.0 würde eine HierarchicalMachine
die Maschineninstanz selbst nicht, sondern die Zustände und Übergänge durch die Erstellung von Kopien davon integrieren. Da jedoch 0,8,0 (Nested)State
Instanzen verwiesen werden, wird die Ansammlung von Zuständen und Ereignissen einer Maschine die andere Maschineninstanz beeinflussen. Modelle und ihr Staat werden jedoch nicht geteilt. Beachten Sie, dass Ereignisse und Übergänge ebenfalls durch Referenz kopiert werden und von beiden Instanzen gemeinsam genutzt werden, wenn Sie das Keyword remap
nicht verwenden. Diese Änderung wurde vorgenommen, um mehr mit Machine
übereinzustimmen, die auch über bestehende State
durch Bezugnahme verwendet wird.
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
Wenn eine HierarchicalMachine
mit dem Schlüsselwort children
übergeben wird, wird der Ausgangszustand dieser Maschine dem neuen übergeordneten Zustand zugewiesen. Im obigen Beispiel sehen wir, dass die Eingabe von counting
auch counting_1
eingibt. Wenn dies ein unerwünschtes Verhalten ist und die Maschine im übergeordneten Status eher stoppen sollte, kann der Benutzer initial
als False
wie {'name': 'counting', 'children': counter, 'initial': False}
bestehen.
Manchmal möchten Sie, dass eine so eingebettete staatliche Sammlung „zurückgibt“, was bedeutet, dass sie nach dem Abschluss und dem Übergang zu einem Ihrer Superstaaten. Um dieses Verhalten zu erreichen, können Sie den Statusübergänge neu gestalten. Im obigen Beispiel möchte wir, dass der Zähler zurückkehren kann, wenn der done
erreicht wurde. Dies geschieht wie folgt:
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
Wie oben erwähnt, kopiert die Verwendung remap
Ereignisse und Übergänge, da sie in der ursprünglichen Statusmaschine nicht gültig sein konnten. Wenn eine wiederverwendete Staatsmaschine keinen endgültigen Zustand hat, können Sie die Übergänge natürlich manuell hinzufügen. Wenn "Zähler" keinen "fertig" Zustand hätte, könnten wir einfach ['done', 'counter_3', 'waiting']
hinzufügen, um das gleiche Verhalten zu erreichen.
In Fällen, in denen Sie möchten, dass Zustände und Übergänge eher durch Wert als Referenz kopiert werden (zum Beispiel, wenn Sie das Verhalten vor 0.8 beibehalten möchten), können Sie dies tun, indem Sie einen NestedState
erstellen und tiefe Kopien der Ereignisse und Zustände der Maschine zuweisen Es.
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 ]
Für komplexe Zustandsmaschinen könnte die Freigabe von Konfigurationen anstelle von sofortigen Maschinen machbarer sein. Zumal instanziierte Maschinen aus HierarchicalMachine
abgeleitet werden müssen. Solche Konfigurationen können einfach über JSON oder YAML gespeichert und geladen werden (siehe FAQ). HierarchicalMachine
ermöglicht das Definieren von Substattern entweder mit den Schlüsselwort children
oder states
. Wenn beide vorhanden sind, werden nur children
berücksichtigt.
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 ()
Zusätzliche Schlüsselwörter:
title
(optional): Legt den Titel des generierten Bildes fest.show_conditions
(Standard falsch): zeigt die Bedingungen an Übergangskanten anshow_auto_transitions
(Standard Falsch): Zeigt automatische Übergänge in der Grafik anshow_state_attributes
(Standard Falsch): Rückrufe anzeigen (eingeben, beenden), Tags und Zeitüberschreitungen in der GrafikÜbergänge können grundlegende Zustandsdiagramme erzeugen, die alle gültigen Übergänge zwischen Zuständen anzeigen. Das Basic -Diagramm -Support erzeugt eine Definition des Meerjungfrauzustands, die mit Mermaids Live -Editor, in Markdown -Dateien in GitLab oder GitHub und anderen Webdiensten verwendet werden kann. Zum Beispiel dieser 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!" )
Erstellt dieses Diagramm (überprüfen Sie die Dokumentquelle, um die Markdown -Notation anzuzeigen):
---
Meerjungfrau -Diagramm
---
Statediagram-V2
Richtung LR
classDEF S_DEFAULT FILL: Weiß, Farbe: Schwarz
classDef s_inactive fill: weiß, Farbe: Schwarz
classDEF s_parallel Farbe: Schwarz, Füllung: Weiß
classDef s_active Farbe: Rot, Füllung: Darksalmon
classDEF s_previous Farbe: Blau, Füllung: Azure
Staat "a" als a
Klasse A S_Previous
Staat "B" als b
Klasse B S_Active
Geben Sie "C" als c
C -> [*]
Klasse C S_DEFAULT
Zustand c {
Status "1" als c_1
Status c_1 {
[*] -> C_1_A
Status "a" als c_1_a
Geben Sie "B" als c_1_b an
C_1_B -> [*]
}
--
Status "2" als c_2
Status c_2 {
[*] -> C_2_A
Geben Sie "a" als c_2_a an
Geben Sie "B" als c_2_b an
C_2_B -> [*]
}
}
C -> A: Zurücksetzen
A -> b: init
B -> C: Tu
C_1_A -> C_1_B: GO
C_2_A -> C_2_B: GO
[*] -> a
Um ausgefeiltere Grafikfunktionen zu verwenden, müssen Sie graphviz
und/oder pygraphviz
installiert haben. Um Diagramme mit dem Paket graphviz
zu generieren, müssen Sie GraphViz manuell oder über einen Paketmanager installieren.
sudo apt-get install graphviz graphviz-dev # Ubuntu and Debian
brew install graphviz # MacOS
conda install graphviz python-graphviz # (Ana)conda
Jetzt können Sie die tatsächlichen Python -Pakete installieren
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
Derzeit wird GraphMachine
pygraphviz
verwenden, wenn er verfügbar ist, und gilt zurück zu graphviz
, wenn pygraphviz
nicht gefunden werden kann. Wenn auch graphviz
nicht verfügbar ist, wird mermaid
verwendet. Dies kann durch Übergabe graph_engine="graphviz"
(oder "mermaid"
) an den Konstruktor überschrieben werden. Beachten Sie, dass sich dieser Standard in Zukunft ändern könnte und die Unterstützung pygraphviz
möglicherweise fallen gelassen werden kann. Mit Model.get_graph()
können Sie den aktuellen Diagramm oder den Bereich von Interesse (ROI) erhalten und sie so zeichnen:
# 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' )
Dies erzeugt so etwas:
Unabhängig vom von Ihnen verwendeten Backend akzeptiert die Draw -Funktion auch einen Dateideskriptor oder einen binären Stream als erstes Argument. Wenn Sie diesen Parameter auf None
festlegen, wird der Byte -Stream zurückgegeben:
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 ()
Referenzen und Teilungen, die als Rückrufe bestanden werden, werden so gut wie möglich gelöst:
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' )
Dies sollte etwas ähnliches erzeugen:
Wenn das Format der Referenzen nicht Ihren Anforderungen entspricht, können Sie die statische Methode GraphMachine.format_references
überschreiben. Wenn Sie die Referenz vollständig überspringen möchten, lassen Sie einfach GraphMachine.format_references
None
. Schauen Sie sich auch unser Beispiel in unserem Beispiel Ipython/Jupyter -Notizbücher an, um ein detaillierteres Beispiel zur Verwendung und Bearbeitung von Grafiken zu erhalten.
In Fällen, in denen das Versenden von Ereignissen in Threads erfolgt, kann man entweder LockedMachine
oder LockedHierarchicalMachine
verwenden, wobei der Funktionszugriff (! Sic) mit Wiedereintrittsschlösser gesichert ist. Dies speichert Sie nicht vor der Verhinderung Ihrer Maschine, indem Sie an Mitgliedervariablen Ihres Modells oder Ihrer Statusmaschine basteln.
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
Jeder Python -Kontext -Manager kann über das Keyword -Argument machine_context
übergeben werden:
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 ])
Alle Kontexte über machine_model
werden zwischen allen mit der Machine
registrierten Modellen gemeinsam genutzt. Es können auch Kontexte pro Model hinzugefügt werden:
lock3 = RLock ()
machine . add_model ( model , model_context = lock3 )
Es ist wichtig, dass alle von Benutzer bereitgestellten Kontextmanager wieder eingetreten sind, da die Statusmaschine sie mehrmals im Kontext eines einzelnen Trigger-Aufrufs aufruft.
Wenn Sie Python 3.7 oder höher verwenden, können Sie AsyncMachine
verwenden, um mit asynchronen Rückernständen zu arbeiten. Sie können synchrone und asynchrone Rückrufe mischen, wenn Sie möchten, aber dies kann unerwünschte Nebenwirkungen haben. Beachten Sie, dass Ereignisse erwartet werden müssen und die Ereignisschleife auch von Ihnen behandelt werden muss.
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 ()
Warum müssen Sie Python 3.7 oder später fragen, ob Sie fragen können. Async -Unterstützung wurde früher eingeführt. AsyncMachine
nutzt contextvars
, um laufende Rückrufe zu verarbeiten, wenn neue Ereignisse ankommen, bevor ein Übergang abgeschlossen ist:
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
Dieses Beispiel zeigt tatsächlich zwei Dinge: Erstens wird das "Go" in M1s Übergang von A
zu B
nicht abgebrochen, und zweitens wird m2.fix()
den Übergangsversuch von M2 von A
nach B
einstellen, indem er "Fix" ausführt, indem er "Fix" ausführt. von A
bis C
. Diese Trennung wäre ohne contextvars
nicht möglich. Beachten Sie, dass prepare
und conditions
nicht als laufende Übergänge behandelt werden. Dies bedeutet, dass nach der Bewertung conditions
ein Übergang ausgeführt wird, obwohl bereits ein anderes Ereignis stattgefunden hat. Aufgaben werden nur beim Ausführen als before
Rückruf oder später abgesagt.
AsyncMachine
verfügt über einen modellspezifischen Warteschlangenmodus, der verwendet werden kann, wenn queued='model'
an den Konstruktor übergeben wird. Bei einer modellspezifischen Warteschlange werden Ereignisse nur dann in die Warteschlange gestellt, wenn sie demselben Modell angehören. Darüber hinaus wird eine erhöhte Ausnahme nur die Ereigniswarteschlange des Modells beseitigen, das diese Ausnahme angehoben hat. Nehmen wir zum Einfachheit halber an, dass jedes Ereignis in asyncio.gather
unten nicht gleichzeitig ausgelöst, sondern leicht verzögert ist:
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
Beachten Sie, dass Warteschlangenmodi nach dem Maschinenbau nicht geändert werden dürfen.
Wenn Ihre Superhelden ein kundenspezifisches Verhalten benötigen, können Sie zusätzliche Funktionen einwerfen, indem Sie Maschinenzustände dekorieren:
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
Derzeit sind Übergänge mit den folgenden Zustandsfunktionen ausgestattet:
Timeout - löst ein Ereignis aus, nachdem einige Zeit vergangen ist
timeout
(int timeout
optional) - Wenn ein eingegebener Status übergeben wirdon_timeout
(String/Callable, Optional) - wird aufgerufen, wenn die Zeitüberschreitungszeit erreicht wurdeAttributeError
, wenn timeout
festgelegt ist, aber on_timeout
nichtTags - fügt Tags den Zuständen hinzu
tags
(Liste, optional) - weist Tags einem Zustand Tags zuState.is_<tag_name>
wird True
zurückkehren, wenn der Status mit tag_name
markiert wurde, sonst False
Fehler - erhöht einen MachineError
, wenn ein Status nicht übrig werden kann
Tags
(wenn Sie Error
verwenden, verwenden Sie keine Tags
)accepted
(bool, optional) - markiert einen Staat wie akzeptierttags
übergeben werden, die "akzeptiert" enthältauto_transitions
auf False
festgelegt wurden. Andernfalls kann jeder Staat mit den Methoden to_<state>
beendet werden.Völliger - initialisiert ein Objekt jedes Mal, wenn ein Zustand eingegeben wird
volatile
(Klasse, Optional) - Jedes Mal, wenn der Status eingegeben wird, wird dem Modell ein Objekt der Typklasse zugewiesen. Der Attributname wird durch hook
definiert. Wenn es weggelassen wird, wird stattdessen ein leeres flüchtiges Objekt erstellthook
(String, default = 'Scope') - Der Attributname des Modells für das zeitliche Objekt. Sie können Ihre eigenen State
schreiben und sie auf die gleiche Weise hinzufügen. Beachten Sie einfach, dass add_state_features
Mixins erwartet. Dies bedeutet, dass Ihre Erweiterung immer die überschriebenen Methoden __init__
aufrufen sollte, enter
und exit
. Ihre Erweiterung kann vom Staat erben, wird aber auch ohne sie funktionieren. Die Verwendung von @add_state_features
hat einen Nachteil, dass dekorierte Maschinen nicht eingelegt werden können (genauer gesagt, der dynamisch erzeugte CustomState
kann nicht eingelegt werden). Dies könnte ein Grund sein, stattdessen eine dedizierte benutzerdefinierte Statusklasse zu schreiben. Abhängig von der ausgewählten Zustandsmaschine muss Ihre benutzerdefinierte Statusklasse möglicherweise bestimmte staatliche Funktionen bereitstellen. HierarchicalMachine
verlangt beispielsweise, dass Ihr benutzerdefinierter Zustand eine Instanz von NestedState
ist ( State
reicht nicht aus). Um Ihre Zustände injizieren, können Sie sie entweder dem state_cls
des Geräts Ihres Machine
zuweisen oder überschreiben Machine.create_state
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 )
Wenn Sie Themen in Ihrer AsyncMachine
vollständig vermeiden möchten, können Sie die Timeout
-Status -Funktion durch AsyncTimeout
aus der asyncio
-Erweiterung ersetzen:
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
Sie sollten in Betracht ziehen, an den TimeoutMachine
-Konstruktor queued=True
zu bestehen. Dies stellt sicher, dass Ereignisse nacheinander verarbeitet werden und asynchrone Rennbedingungen vermeiden, die bei Auszeit und Ereignis in der Nähe auftreten können.
Sie können sich die FAQ ansehen, um Inspiration oder Checkout- django-transitions
zu finden. Es wurde von Christian Ledermann entwickelt und wird auch auf Github veranstaltet. Die Dokumentation enthält einige Verwendungsbeispiele.
Erstens herzlichen Glückwunsch! Sie haben das Ende der Dokumentation erreicht! Wenn Sie transitions
ausprobieren möchten, bevor Sie sie installieren, können Sie dies in einem interaktiven Jupyter -Notebook unter mybinder.org tun. Klicken Sie einfach auf diese Schaltfläche.
Für Fehlerberichte und andere Probleme öffnen Sie bitte ein Problem auf GitHub.
Für Verwendungsfragen posten Sie den Stapelüberlauf und stellen Sie sicher, dass Sie Ihre Frage mit dem pytransitions
-Tag markieren. Vergessen Sie nicht, sich die erweiterten Beispiele anzusehen!
E -Mail an andere Fragen, Anfragen oder große uneingeschränkte Geldgeschenke, E -Mail Tal Yarkoni (Erstautor) und/oder Alexander Neumann (aktueller Betreuer).