Implementasi mesin status berorientasi objek yang ringan dengan Python dengan banyak ekstensi. Kompatibel dengan Python 2.7+ dan 3.0+.
pip install transitions
... atau kloning repo dari GitHub lalu:
python setup.py install
Mereka mengatakan sebuah contoh yang baik bernilai 100 halaman dokumentasi API, sejuta arahan, atau seribu kata.
Yah, "mereka" mungkin berbohong... tapi inilah contohnya:
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?" )
Nah, sekarang Anda telah memasukkan mesin negara ke dalam NarcolepticSuperhero
. Mari kita ajak dia keluar untuk berputar-putar...
> >> 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
Meskipun kita tidak bisa membaca pikiran batman yang sebenarnya, kita pasti bisa memvisualisasikan keadaan NarcolepticSuperhero
kita saat ini.
Lihat ekstensi Diagram jika Anda ingin mengetahui caranya.
Mesin negara adalah model perilaku yang terdiri dari sejumlah negara bagian dan transisi di antara negara-negara tersebut. Dalam setiap keadaan dan transisi, beberapa tindakan dapat dilakukan. Mesin negara perlu memulai pada keadaan awal tertentu. Saat menggunakan transitions
, mesin keadaan dapat terdiri dari beberapa objek yang beberapa ( mesin ) berisi definisi untuk manipulasi ( model ) lainnya. Di bawah ini, kita akan melihat beberapa konsep inti dan cara menggunakannya.
Negara . Sebuah negara mewakili kondisi atau tahapan tertentu dalam mesin negara. Ini adalah mode perilaku atau fase berbeda dalam suatu proses.
Transisi . Ini adalah proses atau peristiwa yang menyebabkan mesin negara berubah dari satu keadaan ke keadaan lainnya.
Model . Struktur stateful sebenarnya. Entitas itulah yang diperbarui selama transisi. Ini juga dapat menentukan tindakan yang akan dijalankan selama transisi. Misalnya, tepat sebelum transisi atau ketika suatu keadaan masuk atau keluar.
Mesin . Ini adalah entitas yang mengelola dan mengontrol model, status, transisi, dan tindakan. Konduktorlah yang mengatur seluruh proses mesin negara.
Pemicu . Ini adalah peristiwa yang memulai transisi, metode yang mengirimkan sinyal untuk memulai transisi.
Tindakan . Operasi atau tugas tertentu yang dilakukan ketika keadaan tertentu masuk, keluar, atau selama transisi. Tindakan ini diimplementasikan melalui callback , yaitu fungsi yang dijalankan ketika suatu peristiwa terjadi.
Mengaktifkan dan menjalankan mesin negara cukup sederhana. Katakanlah Anda memiliki objek lump
(turunan dari kelas Matter
), dan Anda ingin mengelola statusnya:
class Matter ( object ):
pass
lump = Matter ()
Anda dapat menginisialisasi mesin status kerja ( minimal ) yang terikat pada lump
model seperti ini:
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'
Alternatifnya adalah dengan tidak meneruskan model secara eksplisit ke penginisialisasi Machine
:
machine = Machine ( states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ], initial = 'solid' )
# The machine instance itself now acts as a model
machine . state
> >> 'solid'
Perhatikan bahwa kali ini saya tidak menggunakan model lump
sebagai argumen. Argumen pertama yang diteruskan ke Machine
bertindak sebagai model. Jadi ketika saya melewatkan sesuatu di sana, semua fungsi kenyamanan akan ditambahkan ke objek tersebut. Jika tidak ada model yang disediakan maka machine
virtual itu sendiri bertindak sebagai model.
Kalau di awal saya bilang "minimal", itu karena meskipun mesin negara ini secara teknis beroperasi, sebenarnya tidak melakukan apa-apa. Ini dimulai dalam keadaan 'solid'
, tetapi tidak akan pernah berpindah ke keadaan lain, karena belum ada transisi yang ditentukan...!
Ayo coba lagi.
# 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'
Perhatikan metode baru yang melekat pada instance Matter
( evaporate()
, ionize()
, dll.). Setiap metode memicu transisi yang sesuai. Transisi juga dapat dipicu secara dinamis dengan memanggil metode trigger()
yang diberikan dengan nama transisi, seperti yang ditunjukkan di atas. Lebih lanjut tentang ini di bagian Memicu transisi.
Jiwa dari setiap mesin negara yang baik (dan banyak mesin negara yang buruk, tidak diragukan lagi) adalah sekumpulan negara. Di atas, kami mendefinisikan status model yang valid dengan meneruskan daftar string ke penginisialisasi Machine
. Namun secara internal, negara sebenarnya direpresentasikan sebagai objek State
.
Anda dapat menginisialisasi dan mengubah Status dalam beberapa cara. Secara khusus, Anda dapat:
Machine
yang memberikan nama negara bagian, atauState
baru, atauCuplikan berikut menggambarkan beberapa cara untuk mencapai tujuan yang sama:
# 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 ])
Status diinisialisasi satu kali ketika ditambahkan ke mesin dan akan bertahan hingga dihapus dari mesin. Dengan kata lain: jika Anda mengubah atribut suatu objek keadaan, perubahan ini TIDAK akan diatur ulang saat Anda memasukkan keadaan itu lagi. Lihat cara memperluas fitur status jika Anda memerlukan perilaku lain.
Namun hanya memiliki negara bagian dan kemampuan berpindah di antara negara-negara tersebut (transisi) tidaklah berguna dengan sendirinya. Bagaimana jika Anda ingin melakukan sesuatu, melakukan suatu tindakan saat Anda masuk atau keluar dari suatu keadaan? Di sinilah panggilan balik masuk.
Suatu State
juga dapat dikaitkan dengan daftar callback enter
dan exit
, yang dipanggil setiap kali mesin status masuk atau keluar dari status tersebut. Anda dapat menentukan callback selama inisialisasi dengan meneruskannya ke konstruktor objek State
, dalam kamus properti state, atau menambahkannya nanti.
Untuk kenyamanan, setiap kali State
baru ditambahkan ke Machine
, metode on_enter_«state name»
dan on_exit_«state name»
dibuat secara dinamis di Machine (bukan pada model!), yang memungkinkan Anda menambahkan enter dan exit baru secara dinamis panggilan balik nanti jika Anda membutuhkannya.
# 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!'
Perhatikan bahwa panggilan balik on_enter_«state name»
tidak akan diaktifkan saat Mesin pertama kali diinisialisasi. Misalnya jika Anda memiliki panggilan balik on_enter_A()
yang ditentukan, dan menginisialisasi Machine
dengan initial='A'
, on_enter_A()
tidak akan diaktifkan hingga Anda memasuki status A
berikutnya. (Jika Anda perlu memastikan on_enter_A()
diaktifkan saat inisialisasi, Anda cukup membuat status awal tiruan dan kemudian secara eksplisit memanggil to_A()
di dalam metode __init__
.)
Selain meneruskan callback saat menginisialisasi State
, atau menambahkannya secara dinamis, callback juga dapat ditentukan dalam kelas model itu sendiri, yang dapat meningkatkan kejelasan kode. Misalnya:
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' ])
Sekarang, setiap kali transisi lump
ke status A
, metode on_enter_A()
yang ditentukan dalam kelas Matter
akan diaktifkan.
Anda dapat menggunakan callback on_final
yang akan dipicu ketika status dengan final=True
dimasukkan.
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 ...
Anda selalu dapat memeriksa status model saat ini dengan:
.state
, atauis_«state name»()
Dan jika Anda ingin mengambil objek State
sebenarnya untuk status saat ini, Anda dapat melakukannya melalui metode get_state()
instance Machine
.
lump . state
> >> 'solid'
lump . is_gas ()
> >> False
lump . is_solid ()
> >> True
machine . get_state ( lump . state ). name
> >> 'solid'
Jika mau, Anda dapat memilih nama atribut status Anda sendiri dengan meneruskan argumen model_attribute
saat menginisialisasi Machine
. Ini juga akan mengubah nama is_«state name»()
menjadi is_«model_attribute»_«state name»()
. Demikian pula, transisi otomatis akan diberi nama to_«model_attribute»_«state name»()
alih-alih to_«state name»()
. Hal ini dilakukan untuk memungkinkan beberapa mesin bekerja pada model yang sama dengan nama atribut status individual.
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
Sejauh ini kita telah melihat bagaimana kita dapat memberi nama negara bagian dan menggunakan nama tersebut untuk bekerja dengan mesin negara kita. Jika Anda menyukai pengetikan yang lebih ketat dan penyelesaian kode IDE yang lebih banyak (atau Anda tidak dapat mengetik 'sesquipedalophobia' lagi karena kata tersebut membuat Anda takut) menggunakan Enumerasi mungkin adalah yang Anda cari:
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
Anda dapat mencampur enum dan string jika Anda suka (misalnya [States.RED, 'ORANGE', States.YELLOW, States.GREEN]
) tetapi perhatikan bahwa secara internal, transitions
akan tetap menangani status berdasarkan nama ( enum.Enum.name
). Oleh karena itu, tidak mungkin memiliki negara bagian 'GREEN'
dan States.GREEN
pada saat yang bersamaan.
Beberapa contoh di atas sudah mengilustrasikan penggunaan transisi secara sepintas, namun di sini kita akan mengeksplorasinya lebih detail.
Seperti halnya negara bagian, setiap transisi direpresentasikan secara internal sebagai objeknya sendiri – sebuah turunan dari kelas Transition
. Cara tercepat untuk menginisialisasi serangkaian transisi adalah dengan meneruskan kamus, atau daftar kamus, ke penginisialisasi Machine
. Kita sudah melihat ini di atas:
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 )
Mendefinisikan transisi dalam kamus mempunyai manfaat yang jelas, namun dapat menjadi rumit. Jika Anda menginginkan keringkasan, Anda mungkin memilih untuk menentukan transisi menggunakan daftar. Pastikan saja bahwa elemen dalam setiap daftar berada dalam urutan yang sama dengan argumen posisi dalam inisialisasi Transition
(yaitu, trigger
, source
, destination
, dll.).
Daftar-daftar berikut secara fungsional setara dengan daftar kamus di atas:
transitions = [
[ 'melt' , 'solid' , 'liquid' ],
[ 'evaporate' , 'liquid' , 'gas' ],
[ 'sublimate' , 'solid' , 'gas' ],
[ 'ionize' , 'gas' , 'plasma' ]
]
Alternatifnya, Anda dapat menambahkan transisi ke Machine
setelah inisialisasi:
machine = Machine ( model = lump , states = states , initial = 'solid' )
machine . add_transition ( 'melt' , source = 'solid' , dest = 'liquid' )
Agar transisi dapat dijalankan, beberapa peristiwa perlu memicunya . Ada dua cara untuk melakukan ini:
Menggunakan metode yang terlampir secara otomatis di model dasar:
> >> lump . melt ()
> >> lump . state
'liquid'
> >> lump . evaporate ()
> >> lump . state
'gas'
Perhatikan bagaimana Anda tidak perlu mendefinisikan metode ini secara eksplisit di mana pun; nama setiap transisi terikat pada model yang diteruskan ke penginisialisasi Machine
(dalam hal ini, lump
). Ini juga berarti bahwa model Anda tidak boleh berisi metode dengan nama yang sama dengan pemicu peristiwa karena transitions
hanya akan melampirkan metode praktis ke model Anda jika tempatnya belum terisi. Jika Anda ingin mengubah perilaku itu, lihat FAQ.
Menggunakan metode trigger
, sekarang terpasang pada model Anda (jika belum pernah ada sebelumnya). Metode ini memungkinkan Anda menjalankan transisi berdasarkan nama jika diperlukan pemicuan dinamis:
> >> lump . trigger ( 'melt' )
> >> lump . state
'liquid'
> >> lump . trigger ( 'evaporate' )
> >> lump . state
'gas'
Secara default, memicu transisi yang tidak valid akan memunculkan pengecualian:
> >> 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!"
Perilaku ini umumnya diinginkan, karena membantu mengingatkan Anda akan masalah dalam kode Anda. Namun dalam beberapa kasus, Anda mungkin ingin mengabaikan pemicu yang tidak valid secara diam-diam. Anda dapat melakukan ini dengan menyetel ignore_invalid_triggers=True
(baik berdasarkan negara bagian, atau secara global untuk semua negara bagian):
> >> # 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 )
Jika Anda perlu mengetahui transisi mana yang valid dari keadaan tertentu, Anda dapat menggunakan get_triggers
:
m . get_triggers ( 'solid' )
> >> [ 'melt' , 'sublimate' ]
m . get_triggers ( 'liquid' )
> >> [ 'evaporate' ]
m . get_triggers ( 'plasma' )
> >> []
# you can also query several states at once
m . get_triggers ( 'solid' , 'liquid' , 'gas' , 'plasma' )
> >> [ 'melt' , 'evaporate' , 'sublimate' , 'ionize' ]
Jika Anda telah mengikuti dokumentasi ini dari awal, Anda akan melihat bahwa get_triggers
sebenarnya mengembalikan lebih banyak pemicu daripada pemicu yang ditentukan secara eksplisit di atas, seperti to_liquid
dan seterusnya. Ini disebut auto-transitions
dan akan diperkenalkan di bagian berikutnya.
Selain transisi apa pun yang ditambahkan secara eksplisit, metode to_«state»()
dibuat secara otomatis setiap kali status ditambahkan ke instance Machine
. Metode ini bertransisi ke status target, apa pun status mesin saat ini:
lump . to_liquid ()
lump . state
> >> 'liquid'
lump . to_solid ()
lump . state
> >> 'solid'
Jika diinginkan, Anda dapat menonaktifkan perilaku ini dengan mengatur auto_transitions=False
di penginisialisasi Machine
.
Pemicu tertentu dapat dilampirkan ke beberapa transisi, beberapa di antaranya berpotensi dimulai atau diakhiri dalam keadaan yang sama. Misalnya:
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' )
Dalam hal ini, pemanggilan transmogrify()
akan menyetel status model ke 'solid'
jika saat ini 'plasma'
, dan menyetelnya ke 'plasma'
jika tidak. (Perhatikan bahwa hanya transisi pencocokan pertama yang akan dijalankan; dengan demikian, transisi yang ditentukan pada baris terakhir di atas tidak akan menghasilkan apa pun.)
Anda juga dapat membuat pemicu menyebabkan transisi dari semua negara bagian ke tujuan tertentu dengan menggunakan wildcard '*'
:
machine . add_transition ( 'to_liquid' , '*' , 'liquid' )
Perhatikan bahwa transisi karakter pengganti hanya akan berlaku pada status yang ada pada saat panggilan add_transition() . Memanggil transisi berbasis karakter pengganti ketika model berada dalam keadaan yang ditambahkan setelah transisi ditentukan akan menghasilkan pesan transisi yang tidak valid, dan tidak akan bertransisi ke keadaan target.
Pemicu refleksif (pemicu yang memiliki status yang sama dengan sumber dan tujuan) dapat dengan mudah ditambahkan dengan menentukan =
sebagai tujuan. Ini berguna jika pemicu refleksif yang sama harus ditambahkan ke beberapa negara bagian. Misalnya:
machine . add_transition ( 'touch' , [ 'liquid' , 'gas' , 'plasma' ], '=' , after = 'change_shape' )
Ini akan menambahkan transisi refleksif untuk ketiga keadaan dengan touch()
sebagai pemicu dan dengan change_shape
dijalankan setelah setiap pemicu.
Berbeda dengan transisi refleksif, transisi internal tidak akan pernah benar-benar keluar dari negara. Artinya, callback terkait transisi seperti before
atau after
akan diproses, sedangkan callback terkait status exit
atau enter
tidak akan diproses. Untuk menentukan transisi menjadi internal, tetapkan tujuan ke None
.
machine . add_transition ( 'internal' , [ 'liquid' , 'gas' ], None , after = 'change_shape' )
Keinginan umum adalah agar transisi negara mengikuti urutan linier yang ketat. Misalnya, pada keadaan ['A', 'B', 'C']
, Anda mungkin menginginkan transisi yang valid untuk A
→ B
, B
→ C
, dan C
→ A
(tetapi tidak ada pasangan lainnya).
Untuk memfasilitasi perilaku ini, Transitions menyediakan metode add_ordered_transitions()
di kelas Machine
:
states = [ 'A' , 'B' , 'C' ]
# See the "alternative initialization" section for an explanation of the 1st argument to init
machine = Machine ( states = states , initial = 'A' )
machine . add_ordered_transitions ()
machine . next_state ()
print ( machine . state )
> >> 'B'
# We can also define a different order of transitions
machine = Machine ( states = states , initial = 'A' )
machine . add_ordered_transitions ([ 'A' , 'C' , 'B' ])
machine . next_state ()
print ( machine . state )
> >> 'C'
# Conditions can be passed to 'add_ordered_transitions' as well
# If one condition is passed, it will be used for all transitions
machine = Machine ( states = states , initial = 'A' )
machine . add_ordered_transitions ( conditions = 'check' )
# If a list is passed, it must contain exactly as many elements as the
# machine contains states (A->B, ..., X->A)
machine = Machine ( states = states , initial = 'A' )
machine . add_ordered_transitions ( conditions = [ 'check_A2B' , ..., 'check_X2A' ])
# Conditions are always applied starting from the initial state
machine = Machine ( states = states , initial = 'B' )
machine . add_ordered_transitions ( conditions = [ 'check_B2C' , ..., 'check_A2B' ])
# With `loop=False`, the transition from the last state to the first state will be omitted (e.g. C->A)
# When you also pass conditions, you need to pass one condition less (len(states)-1)
machine = Machine ( states = states , initial = 'A' )
machine . add_ordered_transitions ( loop = False )
machine . next_state ()
machine . next_state ()
machine . next_state () # transitions.core.MachineError: "Can't trigger event next_state from state C!"
Perilaku default di Transisi adalah memproses peristiwa secara instan. Ini berarti peristiwa dalam metode on_enter
akan diproses sebelum panggilan balik yang terikat after
dipanggil.
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?
Urutan eksekusi contoh ini adalah
prepare -> before -> on_enter_B -> on_enter_C -> after.
Jika pemrosesan antrean diaktifkan, transisi akan selesai sebelum transisi berikutnya dipicu:
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!
Hal ini mengakibatkan
prepare -> before -> on_enter_B -> queue(to_C) -> after -> on_enter_C.
Catatan penting: saat memproses peristiwa dalam antrean, panggilan pemicu akan selalu mengembalikan True
, karena tidak ada cara untuk menentukan pada waktu antrean apakah transisi yang melibatkan panggilan antrean pada akhirnya akan berhasil diselesaikan. Hal ini berlaku bahkan ketika hanya satu peristiwa yang diproses.
machine . add_transition ( 'jump' , 'A' , 'C' , conditions = 'will_fail' )
...
# queued=False
machine . jump ()
> >> False
# queued=True
machine . jump ()
> >> True
Saat model dihapus dari mesin, transitions
juga akan menghapus semua kejadian terkait dari antrean.
class Model :
def on_enter_B ( self ):
self . to_C () # add event to queue ...
self . machine . remove_model ( self ) # aaaand it's gone
Terkadang Anda hanya ingin transisi tertentu dijalankan jika kondisi tertentu terjadi. Anda dapat melakukan ini dengan meneruskan suatu metode, atau daftar metode, dalam argumen conditions
:
# Our Matter class, now with a bunch of methods that return booleans.
class Matter ( object ):
def is_flammable ( self ): return False
def is_really_hot ( self ): return True
machine . add_transition ( 'heat' , 'solid' , 'gas' , conditions = 'is_flammable' )
machine . add_transition ( 'heat' , 'solid' , 'liquid' , conditions = [ 'is_really_hot' ])
Dalam contoh di atas, pemanggilan heat()
saat model dalam keadaan 'solid'
akan bertransisi ke keadaan 'gas'
jika is_flammable
mengembalikan True
. Jika tidak, ia akan bertransisi ke status 'liquid'
jika is_really_hot
mengembalikan True
.
Untuk kenyamanan, ada juga argumen 'unless'
yang berperilaku persis seperti kondisi, namun terbalik:
machine . add_transition ( 'heat' , 'solid' , 'gas' , unless = [ 'is_flammable' , 'is_really_hot' ])
Dalam hal ini, model akan bertransisi dari padat ke gas setiap kali heat()
menyala, asalkan is_flammable()
dan is_really_hot()
mengembalikan False
.
Perhatikan bahwa metode pemeriksaan kondisi akan secara pasif menerima argumen opsional dan/atau objek data yang diteruskan ke metode pemicu. Misalnya, panggilan berikut:
lump . heat ( temp = 74 )
# equivalent to lump.trigger('heat', temp=74)
... akan meneruskan kwarg opsional temp=74
ke pemeriksaan is_flammable()
(mungkin dibungkus dalam instance EventData
). Untuk informasi lebih lanjut tentang ini, lihat bagian Melewati data di bawah.
Jika Anda ingin memastikan transisi dapat dilakukan sebelum melanjutkannya, Anda dapat menggunakan fungsi may_<trigger_name>
yang telah ditambahkan ke model Anda. Model Anda juga berisi fungsi may_trigger
untuk memeriksa pemicu berdasarkan nama:
# check if the current temperature is hot enough to trigger a transition
if lump . may_heat ():
# if lump.may_trigger("heat"):
lump . heat ()
Ini akan mengeksekusi semua panggilan balik prepare
dan mengevaluasi kondisi yang ditetapkan untuk transisi potensial. Pemeriksaan transisi juga dapat digunakan ketika tujuan transisi tidak tersedia (belum):
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
Anda dapat melampirkan callback ke transisi dan juga status. Setiap transisi memiliki atribut 'before'
dan 'after'
yang berisi daftar metode yang dipanggil sebelum dan sesudah transisi dijalankan:
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?"
Ada juga panggilan balik 'prepare'
yang dijalankan segera setelah transisi dimulai, sebelum 'conditions'
apa pun diperiksa atau panggilan balik lainnya dijalankan.
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!"
Perhatikan bahwa prepare
tidak akan dipanggil kecuali keadaan saat ini merupakan sumber yang valid untuk transisi bernama.
Tindakan default yang dimaksudkan untuk dijalankan sebelum atau sesudah setiap transisi dapat diteruskan ke Machine
selama inisialisasi dengan masing-masing before_state_change
dan after_state_change
:
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?"
Ada juga dua kata kunci untuk callback yang harus dieksekusi secara independen a) berapa banyak transisi yang mungkin, b) jika ada transisi yang berhasil dan c) bahkan jika kesalahan muncul selama eksekusi beberapa callback lainnya. Callback yang diteruskan ke Machine
dengan prepare_event
akan dieksekusi satu kali sebelum memproses kemungkinan transisi (dan callback prepare
individualnya) dilakukan. Callback finalize_event
akan dieksekusi terlepas dari keberhasilan transisi yang diproses. Perhatikan bahwa jika terjadi kesalahan maka akan dilampirkan ke event_data
sebagai error
dan dapat diambil dengan send_event=True
.
from transitions import Machine
class Matter ( object ):
def raise_error ( self , event ): raise ValueError ( "Oh no" )
def prepare ( self , event ): print ( "I am ready!" )
def finalize ( self , event ): print ( "Result: " , type ( event . error ), event . error )
states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ]
lump = Matter ()
m = Machine ( lump , states , prepare_event = 'prepare' , before_state_change = 'raise_error' ,
finalize_event = 'finalize' , send_event = True )
try :
lump . to_gas ()
except ValueError :
pass
print ( lump . state )
# >>> I am ready!
# >>> Result: <class 'ValueError'> Oh no
# >>> initial
Terkadang segala sesuatunya tidak berjalan sebagaimana mestinya dan kita perlu menangani pengecualian dan membereskan kekacauan agar segala sesuatunya tetap berjalan. Kita dapat meneruskan panggilan balik ke on_exception
untuk melakukan ini:
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
Seperti yang mungkin sudah Anda sadari, cara standar meneruskan callable ke status, kondisi, dan transisi adalah dengan nama. Saat memproses callback dan ketentuan, transitions
akan menggunakan namanya untuk mengambil callable terkait dari model. Jika metode tidak dapat diambil dan berisi titik, transitions
akan memperlakukan nama tersebut sebagai jalur ke fungsi modul dan mencoba mengimpornya. Alternatifnya, Anda dapat memberikan nama properti atau atribut. Mereka akan digabungkan ke dalam fungsi tetapi tidak dapat menerima data peristiwa karena alasan yang jelas. Anda juga dapat meneruskan callable seperti fungsi (terikat) secara langsung. Seperti disebutkan sebelumnya, Anda juga dapat meneruskan daftar/tupel nama callable ke parameter panggilan balik. Callback akan dieksekusi sesuai urutan penambahannya.
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 ()
Resolusi yang dapat dipanggil dilakukan di Machine.resolve_callable
. Metode ini dapat dikesampingkan jika diperlukan strategi penyelesaian yang lebih rumit dan dapat dipanggil.
Contoh
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 )
Singkatnya, saat ini ada tiga cara untuk memicu peristiwa. Anda dapat memanggil fungsi kenyamanan model seperti lump.melt()
, mengeksekusi pemicu berdasarkan nama seperti lump.trigger("melt")
atau mengirimkan peristiwa pada beberapa model dengan machine.dispatch("melt")
(lihat bagian tentang beberapa model di pola inisialisasi alternatif). Callback pada transisi kemudian dieksekusi dalam urutan berikut:
Panggilan balik | Keadaan Saat Ini | Komentar |
---|---|---|
'machine.prepare_event' | source | dieksekusi satu kali sebelum transisi individual diproses |
'transition.prepare' | source | dieksekusi segera setelah transisi dimulai |
'transition.conditions' | source | kondisi mungkin gagal dan menghentikan transisi |
'transition.unless' | source | kondisi mungkin gagal dan menghentikan transisi |
'machine.before_state_change' | source | panggilan balik default dideklarasikan pada model |
'transition.before' | source | |
'state.on_exit' | source | panggilan balik dideklarasikan pada status sumber |
<STATE CHANGE> | ||
'state.on_enter' | destination | panggilan balik dideklarasikan pada negara tujuan |
'transition.after' | destination | |
'machine.on_final' | destination | panggilan balik pada anak-anak akan dipanggil terlebih dahulu |
'machine.after_state_change' | destination | callback default dideklarasikan pada model; juga akan dipanggil setelah transisi internal |
'machine.on_exception' | source/destination | panggilan balik akan dieksekusi ketika pengecualian telah dimunculkan |
'machine.finalize_event' | source/destination | panggilan balik akan dieksekusi meskipun tidak ada transisi yang terjadi atau pengecualian telah dimunculkan |
Jika ada panggilan balik yang menimbulkan pengecualian, pemrosesan panggilan balik tidak dilanjutkan. Ini berarti bahwa ketika kesalahan terjadi sebelum transisi (di state.on_exit
atau sebelumnya), kesalahan tersebut dihentikan. Jika ada kenaikan setelah transisi dilakukan (di state.on_enter
atau lebih baru), perubahan status akan tetap ada dan tidak ada rollback yang terjadi. Callback yang ditentukan dalam machine.finalize_event
akan selalu dieksekusi kecuali pengecualian dimunculkan oleh callback finalisasi itu sendiri. Perhatikan bahwa setiap urutan panggilan balik harus diselesaikan sebelum tahap berikutnya dijalankan. Memblokir panggilan balik akan menghentikan perintah eksekusi dan karenanya memblokir panggilan trigger
atau dispatch
itu sendiri. Jika Anda ingin callback dieksekusi secara paralel, Anda dapat melihat ekstensi AsyncMachine
untuk pemrosesan asinkron atau LockedMachine
untuk threading.
Terkadang Anda perlu meneruskan fungsi panggilan balik yang terdaftar pada inisialisasi mesin beberapa data yang mencerminkan status model saat ini. Transisi memungkinkan Anda melakukan ini dengan dua cara berbeda.
Pertama (default), Anda dapat meneruskan argumen posisi atau kata kunci apa pun langsung ke metode pemicu (dibuat saat Anda memanggil add_transition()
):
class Matter ( object ):
def __init__ ( self ): self . set_environment ()
def set_environment ( self , temp = 0 , pressure = 101.325 ):
self . temp = temp
self . pressure = pressure
def print_temperature ( self ): print ( "Current temperature is %d degrees celsius." % self . temp )
def print_pressure ( self ): print ( "Current pressure is %.2f kPa." % self . pressure )
lump = Matter ()
machine = Machine ( lump , [ 'solid' , 'liquid' ], initial = 'solid' )
machine . add_transition ( 'melt' , 'solid' , 'liquid' , before = 'set_environment' )
lump . melt ( 45 ) # positional arg;
# equivalent to lump.trigger('melt', 45)
lump . print_temperature ()
> >> 'Current temperature is 45 degrees celsius.'
machine . set_state ( 'solid' ) # reset state so we can melt again
lump . melt ( pressure = 300.23 ) # keyword args also work
lump . print_pressure ()
> >> 'Current pressure is 300.23 kPa.'
Anda dapat meneruskan sejumlah argumen yang Anda suka ke pemicunya.
Ada satu batasan penting pada pendekatan ini: setiap fungsi panggilan balik yang dipicu oleh transisi keadaan harus mampu menangani semua argumen. Hal ini dapat menimbulkan masalah jika masing-masing callback mengharapkan data yang agak berbeda.
Untuk menyiasatinya, Transitions mendukung metode alternatif untuk mengirim data. Jika Anda menyetel send_event=True
pada inisialisasi Machine
, semua argumen ke pemicu akan digabungkan dalam instance EventData
dan diteruskan ke setiap panggilan balik. (Objek EventData
juga menyimpan referensi internal ke status sumber, model, transisi, mesin, dan pemicu yang terkait dengan peristiwa tersebut, jika Anda perlu mengaksesnya untuk apa pun.)
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.'
Dalam semua contoh sejauh ini, kami telah melampirkan instance Machine
baru ke model terpisah ( lump
, sebuah instance dari kelas Matter
). Meskipun pemisahan ini menjaga semuanya tetap rapi (karena Anda tidak perlu menambal sejumlah metode baru ke dalam kelas Matter
), hal ini juga dapat mengganggu, karena mengharuskan Anda untuk melacak metode mana yang dipanggil pada mesin negara. , dan mana yang dipanggil pada model yang terikat dengan mesin negara (misalnya, lump.on_enter_StateA()
vs. machine.add_transition()
).
Untungnya, Transisi fleksibel dan mendukung dua pola inisialisasi lainnya.
Pertama, Anda dapat membuat mesin status mandiri yang tidak memerlukan model lain sama sekali. Cukup hilangkan argumen model selama inisialisasi:
machine = Machine ( states = states , transitions = transitions , initial = 'solid' )
machine . melt ()
machine . state
> >> 'liquid'
Jika Anda menginisialisasi mesin dengan cara ini, Anda kemudian dapat melampirkan semua peristiwa pemicu (seperti evaporate()
, sublimate()
, dll.) dan semua fungsi callback langsung ke instance Machine
.
Pendekatan ini memiliki keuntungan dalam menggabungkan semua fungsionalitas mesin status di satu tempat, namun bisa terasa sedikit tidak wajar jika menurut Anda logika status harus dimasukkan ke dalam model itu sendiri, bukan di pengontrol terpisah.
Pendekatan alternatif (yang berpotensi lebih baik) adalah dengan menjadikan model tersebut mewarisi kelas Machine
. Transisi dirancang untuk mendukung pewarisan dengan lancar. (pastikan untuk mengganti metode __init__
class Machine
!):
class Matter ( Machine ):
def say_hello ( self ): print ( "hello, new state!" )
def say_goodbye ( self ): print ( "goodbye, old state!" )
def __init__ ( self ):
states = [ 'solid' , 'liquid' , 'gas' ]
Machine . __init__ ( self , states = states , initial = 'solid' )
self . add_transition ( 'melt' , 'solid' , 'liquid' )
lump = Matter ()
lump . state
> >> 'solid'
lump . melt ()
lump . state
> >> 'liquid'
Di sini Anda dapat menggabungkan semua fungsionalitas mesin status ke dalam model yang sudah ada, yang sering kali terasa lebih alami daripada memasukkan semua fungsi yang kita inginkan ke dalam instance Machine
mandiri yang terpisah.
Sebuah mesin dapat menangani beberapa model yang dapat diteruskan sebagai daftar seperti Machine(model=[model1, model2, ...])
. Jika Anda ingin menambahkan model serta instance mesin itu sendiri, Anda dapat meneruskan variabel kelas placeholder (string) Machine.self_literal
selama inisialisasi seperti Machine(model=[Machine.self_literal, model1, ...])
. Anda juga dapat membuat mesin mandiri, dan mendaftarkan model secara dinamis melalui machine.add_model
dengan meneruskan model=None
ke konstruktor. Selanjutnya, Anda dapat menggunakan machine.dispatch
untuk memicu peristiwa pada semua model yang ditambahkan saat ini. Ingatlah untuk memanggil machine.remove_model
jika mesin tahan lama dan model Anda bersifat sementara dan harus dikumpulkan dari sampah:
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
Jika Anda tidak memberikan status awal di konstruktor mesin status, transitions
akan membuat dan menambahkan status default yang disebut 'initial'
. Jika Anda tidak menginginkan keadaan awal default, Anda dapat meneruskan initial=None
. Namun, dalam hal ini Anda harus melewati keadaan awal setiap kali Anda menambahkan model.
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' )
Model dengan banyak status dapat melampirkan beberapa mesin menggunakan nilai model_attribute
yang berbeda. Seperti yang disebutkan dalam status Pemeriksaan, ini akan menambahkan fungsi is/to_<model_attribute>_<state_name>
khusus:
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
Transisi mencakup kemampuan logging yang sangat mendasar. Sejumlah peristiwa – yaitu, perubahan status, pemicu transisi, dan pemeriksaan kondisional – dicatat sebagai peristiwa tingkat INFO menggunakan modul logging
Python standar. Ini berarti Anda dapat dengan mudah mengonfigurasi logging ke output standar dalam skrip:
# 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' )
...
Mesin dapat diasinkan dan dapat disimpan serta diisi dengan pickle
. Untuk Python 3.3 dan sebelumnya diperlukan dill
.
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' ]
Seperti yang mungkin Anda ketahui, transitions
menggunakan beberapa fitur dinamis Python untuk memberi Anda cara praktis menangani model. Namun, pemeriksa tipe statis tidak menyukai atribut model dan metode yang tidak diketahui sebelum waktu proses. Secara historis, transitions
juga tidak menetapkan metode kenyamanan yang sudah ditentukan pada model untuk mencegah penggantian yang tidak disengaja.
Tapi jangan khawatir! Anda dapat menggunakan parameter konstruktor mesin model_override
untuk mengubah cara model didekorasi. Jika Anda menyetel model_override=True
, transitions
hanya akan menimpa metode yang sudah ditentukan. Hal ini mencegah metode baru muncul saat runtime dan juga memungkinkan Anda menentukan metode pembantu mana yang ingin Anda gunakan.
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"
Jika Anda ingin menggunakan semua fungsi praktis dan memasukkan beberapa callback ke dalamnya, mendefinisikan model bisa menjadi sangat rumit ketika Anda memiliki banyak status dan transisi yang ditentukan. Metode generate_base_model
dalam transitions
dapat menghasilkan model dasar dari konfigurasi mesin untuk membantu Anda melakukannya.
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 )
Mendefinisikan metode model yang akan diganti menambah sedikit kerja ekstra. Mungkin sulit untuk beralih bolak-balik untuk memastikan nama peristiwa dieja dengan benar, terutama jika status dan transisi ditentukan dalam daftar sebelum atau sesudah model Anda. Anda dapat mengurangi boilerplate dan ketidakpastian bekerja dengan string dengan mendefinisikan status sebagai enum. Anda juga dapat menentukan transisi langsung di kelas model Anda dengan bantuan add_transitions
dan event
. Terserah Anda apakah Anda menggunakan dekorator fungsi add_transitions
atau event untuk menetapkan nilai pada atribut bergantung pada gaya kode pilihan Anda. Keduanya bekerja dengan cara yang sama, memiliki tanda tangan yang sama, dan seharusnya menghasilkan (hampir) petunjuk tipe IDE yang sama. Karena ini masih dalam proses, Anda harus membuat kelas Mesin khusus dan menggunakan with_model_definitions untuk transisi guna memeriksa transisi yang ditentukan seperti itu.
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
Meskipun inti transisi tetap ringan, ada beragam MixIn untuk memperluas fungsinya. Saat ini didukung adalah:
Ada dua mekanisme untuk mengambil instance mesin status dengan fitur yang diinginkan diaktifkan. Pendekatan pertama memanfaatkan factory
praktis dengan empat parameter graph
, nested
, locked
atau asyncio
yang disetel ke True
jika fitur tersebut diperlukan:
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 )
Pendekatan ini menargetkan penggunaan eksperimental karena dalam hal ini kelas yang mendasarinya tidak harus diketahui. Namun, kelas juga dapat diimpor langsung dari transitions.extensions
. Skema penamaannya adalah sebagai berikut:
Diagram | Bersarang | Terkunci | Asincio | |
---|---|---|---|---|
Mesin | ✘ | ✘ | ✘ | ✘ |
Mesin Grafik | ✓ | ✘ | ✘ | ✘ |
Mesin Hierarki | ✘ | ✓ | ✘ | ✘ |
Mesin Terkunci | ✘ | ✘ | ✓ | ✘ |
Mesin Grafik Hierarki | ✓ | ✓ | ✘ | ✘ |
Mesin Grafik Terkunci | ✓ | ✘ | ✓ | ✘ |
Mesin Hirarki Terkunci | ✘ | ✓ | ✓ | ✘ |
Mesin Grafik Hierarki Terkunci | ✓ | ✓ | ✓ | ✘ |
Mesin Asinkron | ✘ | ✘ | ✘ | ✓ |
Mesin Grafik Async | ✓ | ✘ | ✘ | ✓ |
Mesin Async Hierarki | ✘ | ✓ | ✘ | ✓ |
Mesin Grafik Async Hierarki | ✓ | ✓ | ✘ | ✓ |
Untuk menggunakan mesin negara yang kaya fitur, seseorang dapat menulis:
from transitions . extensions import LockedHierarchicalGraphMachine as LHGMachine
machine = LHGMachine ( model , states , transitions )
Transisi menyertakan modul ekstensi yang memungkinkan status bersarang. Hal ini memungkinkan kami membuat konteks dan memodelkan kasus di mana status terkait dengan subtugas tertentu di mesin status. Untuk membuat status bersarang, impor NestedState
dari transisi atau gunakan kamus dengan argumen inisialisasi name
dan children
. Secara opsional, initial
dapat digunakan untuk menentukan sub negara bagian yang akan dijadikan tujuan transit, ketika keadaan bersarang dimasukkan.
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')
Konfigurasi yang menggunakan initial
akan terlihat seperti ini:
# ...
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' ]
]
# ...
Kata kunci initial
dari konstruktor HierarchicalMachine
menerima status bersarang (misal: initial='caffeinated_running'
) dan daftar status yang dianggap sebagai status paralel (misal: initial=['A', 'B']
) atau status saat ini dari model lain ( initial=model.state
) yang seharusnya menjadi salah satu opsi yang disebutkan sebelumnya. Perhatikan bahwa saat meneruskan string, transition
akan memeriksa status target untuk substat initial
dan menggunakannya sebagai status entri. Hal ini akan dilakukan secara rekursif sampai suatu substate tidak menyebutkan keadaan awal. Negara-negara paralel atau negara yang disahkan sebagai daftar akan digunakan 'sebagaimana adanya' dan tidak ada evaluasi awal lebih lanjut yang akan dilakukan.
Perhatikan bahwa objek status yang Anda buat sebelumnya harus berupa NestedState
atau kelas turunannya. Kelas State
standar yang digunakan dalam instance Machine
sederhana tidak memiliki fitur yang diperlukan untuk nesting.
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!
Beberapa hal yang harus diperhatikan saat bekerja dengan status bersarang: Nama negara bagian digabungkan dengan NestedState.separator
. Saat ini pemisah disetel ke garis bawah ('_') dan oleh karena itu berperilaku serupa dengan mesin dasar. Ini berarti substate bar
dari state foo
akan diketahui oleh foo_bar
. baz
bar
subnegara bagian akan disebut sebagai foo_bar_baz
dan seterusnya. Saat memasuki suatu subnegara bagian, enter
akan dipanggil untuk semua negara bagian induk. Hal yang sama juga berlaku untuk sub-negara bagian yang keluar. Ketiga, negara bagian yang tersarang dapat menimpa perilaku transisi orang tuanya. Jika suatu transisi tidak diketahui oleh keadaan saat ini, maka transisi tersebut akan didelegasikan kepada induknya.
Artinya dalam konfigurasi standar, nama status di HSM TIDAK BOLEH mengandung garis bawah. Untuk transitions
tidak mungkin untuk mengetahui apakah machine.add_state('state_name')
harus menambahkan status bernama state_name
atau menambahkan name
substat ke status state
. Namun dalam beberapa kasus, hal ini tidak cukup. Misalnya jika nama negara bagian terdiri dari lebih dari satu kata dan Anda ingin/perlu menggunakan garis bawah untuk memisahkannya, bukan CamelCase
. Untuk mengatasinya, kalian bisa mengubah karakter yang digunakan untuk melakukan pemisahan dengan cukup mudah. Anda bahkan dapat menggunakan karakter unicode yang mewah jika Anda menggunakan Python 3. Menyetel pemisah ke selain garis bawah akan mengubah beberapa perilaku (transisi_otomatis dan menyetel panggilan balik):
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')
Alih-alih to_C_3_a()
transisi otomatis disebut sebagai to_C.s3.a()
. Jika substatus Anda dimulai dengan angka, transisi menambahkan awalan 's' ('3' menjadi 's3') ke FunctionWrapper
transisi otomatis untuk mematuhi skema penamaan atribut Python. Jika penyelesaian interaktif tidak diperlukan, to('C↦3↦a')
dapat dipanggil secara langsung. Selain itu, on_enter/exit_<<state name>>
diganti dengan on_enter/exit(state_name, callback)
. Pemeriksaan negara dapat dilakukan dengan cara yang sama. Daripada is_C_3_a()
, varian FunctionWrapper
is_C.s3.a()
dapat digunakan.
Untuk memeriksa apakah keadaan saat ini merupakan subkeadaan dari keadaan tertentu, is_state
mendukung kata 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
Anda juga dapat menggunakan enumerasi di HSM, namun perlu diingat bahwa Enum
dibandingkan berdasarkan nilai. Jika Anda memiliki nilai lebih dari satu kali dalam pohon status, status tersebut tidak dapat dibedakan.
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
telah ditulis ulang dari awal untuk mendukung status paralel dan isolasi status bersarang yang lebih baik. Ini melibatkan beberapa penyesuaian berdasarkan masukan komunitas. Untuk mendapatkan gambaran tentang urutan pemrosesan dan konfigurasi, lihat contoh berikut:
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
Saat menggunakan parallel
alih-alih children
, transitions
akan memasuki semua status daftar yang dilewati secara bersamaan. Subnegara bagian mana yang akan dimasuki ditentukan oleh initial
yang harus selalu menunjuk ke subnegara bagian langsung. Fitur barunya adalah mendefinisikan transisi lokal dengan meneruskan kata kunci transitions
dalam definisi keadaan. Transisi ['go', 'a', 'b']
yang ditentukan di atas hanya valid di C_1
. Meskipun Anda dapat mereferensikan substat seperti yang dilakukan di ['go', '2_z', '2_x']
Anda tidak dapat mereferensikan status induk secara langsung dalam transisi yang ditentukan secara lokal. Ketika negara induk keluar, anak-anaknya juga akan keluar. Selain urutan pemrosesan transisi yang diketahui dari Machine
di mana transisi dipertimbangkan sesuai urutan penambahannya, HierarchicalMachine
juga mempertimbangkan hierarki. Transisi yang didefinisikan dalam substat akan dievaluasi terlebih dahulu (misalnya C_1_a
dibiarkan sebelum C_2_z
) dan transisi yang ditentukan dengan wildcard *
akan (untuk saat ini) hanya menambahkan transisi ke status root (dalam contoh ini A
, B
, C
) dimulai dengan 0,8,0 negara bersarang dapat ditambahkan secara langsung dan akan mengeluarkan penciptaan negara induk saat terbang:
m = HierarchicalMachine ( states = [ 'A' ], initial = 'A' )
m . add_state ( 'B_1_a' )
m . to_B_1 ()
assert m . is_B ( allow_substates = True )
Eksperimental di 0.9.1: Anda dapat menggunakan callback on_final
baik di negara bagian atau di HSM itu sendiri. Callbacks akan dipicu jika a) keadaan itu sendiri ditandai dengan final
dan baru saja dimasukkan atau b) semua substat dianggap final dan setidaknya satu substat baru saja memasuki keadaan akhir. Dalam hal b) semua orang tua akan dianggap final juga jika kondisi b) berlaku untuk mereka. Ini mungkin berguna dalam kasus di mana pemrosesan terjadi secara paralel dan HSM Anda atau keadaan induk harus diberitahu ketika semua substat telah mencapai keadaan akhir:
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!
Selain pesanan semantik, negara -negara bersarang sangat berguna jika Anda ingin menentukan mesin negara untuk tugas -tugas tertentu dan berencana untuk menggunakannya kembali. Sebelum 0.8.0 , HierarchicalMachine
tidak akan mengintegrasikan contoh mesin itu sendiri tetapi negara bagian dan transisi dengan membuat salinannya. Namun, karena keadaan negara 0,8.0 (Nested)State
hanya direferensikan yang berarti perubahan dalam koleksi satu mesin dan peristiwa akan mempengaruhi contoh mesin lainnya. Model dan keadaan mereka tidak akan dibagikan. Perhatikan bahwa peristiwa dan transisi juga disalin dengan referensi dan akan dibagikan oleh kedua contoh jika Anda tidak menggunakan kata kunci remap
. Perubahan ini dilakukan untuk lebih sesuai dengan Machine
yang juga menggunakan instance State
yang dilewati dengan referensi.
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
Jika HierarchicalMachine
dilewatkan dengan kata kunci children
, keadaan awal mesin ini akan ditugaskan ke negara induk yang baru. Dalam contoh di atas kita melihat bahwa masuk counting
juga akan masuk counting_1
. Jika ini adalah perilaku yang tidak diinginkan dan mesin lebih baik berhenti dalam keadaan induk, pengguna dapat meneruskan initial
sebagai False
seperti {'name': 'counting', 'children': counter, 'initial': False}
.
Kadang -kadang Anda ingin koleksi keadaan tertanam seperti itu 'mengembalikan' yang berarti setelah dilakukan, ia harus keluar dan transit ke salah satu negara super Anda. Untuk mencapai perilaku ini, Anda dapat memetakan kembali transisi negara. Dalam contoh di atas kami ingin konter untuk kembali jika negara done
tercapai. Ini dilakukan sebagai berikut:
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
Seperti disebutkan di atas, menggunakan remap
akan menyalin acara dan transisi karena mereka tidak dapat valid di mesin negara asli. Jika mesin negara yang digunakan kembali tidak memiliki keadaan akhir, tentu saja Anda dapat menambahkan transisi secara manual. Jika 'counter' tidak memiliki keadaan 'selesai', kami bisa menambahkan ['done', 'counter_3', 'waiting']
untuk mencapai perilaku yang sama.
Dalam kasus di mana Anda ingin status dan transisi disalin dengan nilai daripada referensi (misalnya, jika Anda ingin menjaga perilaku pra-0.8) Anda dapat melakukannya dengan membuat NestedState
dan menugaskan salinan mendalam dari peristiwa dan negara bagian mesin untuk dia.
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 ]
Untuk mesin negara yang kompleks, berbagi konfigurasi daripada mesin instantiated mungkin lebih layak. Terutama karena mesin instantiated harus berasal dari HierarchicalMachine
. Konfigurasi semacam itu dapat disimpan dan dimuat dengan mudah melalui JSON atau YAML (lihat FAQ). HierarchicalMachine
memungkinkan mendefinisikan substat baik dengan kata kunci children
atau states
. Jika keduanya hadir, hanya children
yang akan dipertimbangkan.
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 ()
Kata kunci tambahan:
title
(Opsional): Menetapkan judul gambar yang dihasilkan.show_conditions
(default false): menunjukkan kondisi di tepi transisishow_auto_transitions
(default false): menunjukkan transisi otomatis dalam grafikshow_state_attributes
(default false): tunjukkan callbacks (enter, exit), tag dan timeout dalam grafikTransisi dapat menghasilkan diagram keadaan dasar yang menampilkan semua transisi yang valid antar negara. Dukungan diagram dasar menghasilkan definisi mesin negara bagian putri duyung yang dapat digunakan dengan editor langsung putri duyung, dalam file -file markdown di gitlab atau github dan layanan web lainnya. Misalnya, kode ini:
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!" )
Menghasilkan diagram ini (periksa sumber dokumen untuk melihat notasi penurunan harga):
---
Grafik Putri Duyung
---
STATEDIAGRAM-V2
arah lr
ClassDef S_Default Fill: White, Color: Black
ClassDef S_inactive Fill: Putih, Warna: Hitam
ClassDef S_PARALLEL Warna: Hitam, Isi: Putih
ClassDef S_Active Color: Red, Fill: Darksalmon
ClassDef S_Previous Warna: Biru, Isi: Azure
menyatakan "a" sebagai a
Kelas A S_Previous
menyatakan "b" sebagai b
Kelas B S_Active
menyatakan "c" sebagai c
C -> [*]
Kelas C S_Default
status c {
menyatakan "1" sebagai c_1
status c_1 {
[*] -> c_1_a
Nyatakan "A" sebagai C_1_A
Nyatakan "B" sebagai C_1_B
C_1_b -> [*]
}
--
menyatakan "2" sebagai c_2
status c_2 {
[*] -> C_2_A
Nyatakan "A" sebagai C_2_A
Nyatakan "B" sebagai C_2_B
C_2_b -> [*]
}
}
C -> A: Reset
A -> B: init
B -> C: lakukan
C_1_a -> c_1_b: go
C_2_a -> c_2_b: go
[*] -> a
Untuk menggunakan fungsionalitas grafik yang lebih canggih, Anda harus menginstal graphviz
dan/atau pygraphviz
. Untuk menghasilkan grafik dengan paket graphviz
, Anda perlu menginstal GraphViz secara manual atau melalui manajer paket.
sudo apt-get install graphviz graphviz-dev # Ubuntu and Debian
brew install graphviz # MacOS
conda install graphviz python-graphviz # (Ana)conda
Sekarang Anda dapat memasang paket Python yang sebenarnya
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
Saat ini, GraphMachine
akan menggunakan pygraphviz
ketika tersedia dan kembali ke graphviz
ketika pygraphviz
tidak dapat ditemukan. Jika graphviz
juga tidak tersedia, mermaid
akan digunakan. Ini dapat ditimpa dengan melewati graph_engine="graphviz"
(atau "mermaid"
) ke konstruktor. Perhatikan bahwa default ini mungkin berubah di masa depan dan dukungan pygraphviz
dapat dijatuhkan. Dengan Model.get_graph()
Anda bisa mendapatkan grafik saat ini atau wilayah yang menarik (ROI) dan menggambarnya seperti ini:
# 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' )
Ini menghasilkan sesuatu seperti ini:
Terlepas dari backend yang Anda gunakan, fungsi Draw juga menerima deskriptor file atau aliran biner sebagai argumen pertama. Jika Anda mengatur parameter ini ke None
, aliran byte akan dikembalikan:
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 ()
Referensi dan sebagian disahkan sebagai panggilan balik akan diselesaikan sebaik mungkin:
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' )
Ini seharusnya menghasilkan sesuatu yang mirip dengan ini:
Jika format referensi tidak sesuai dengan kebutuhan Anda, Anda dapat mengganti metode statis GraphMachine.format_references
. Jika Anda ingin melewatkan referensi sepenuhnya, biarkan saja GraphMachine.format_references
tidak mengembalikan None
. Juga, lihat contoh kami Ipython/Jupyter Notebook untuk contoh yang lebih rinci tentang cara menggunakan dan mengedit grafik.
Dalam kasus di mana pengiriman peristiwa dilakukan di utas, seseorang dapat menggunakan LockedMachine
atau LockedHierarchicalMachine
di mana akses fungsi (! SIC) diamankan dengan kunci masuk kembali. Ini tidak menyelamatkan Anda dari merusak mesin Anda dengan mengutak -atik variabel anggota model atau mesin negara Anda.
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
Manajer konteks Python mana pun dapat dilewati melalui argumen kata kunci machine_context
:
from transitions . extensions import LockedMachine
from threading import RLock
states = [ 'A' , 'B' , 'C' ]
lock1 = RLock ()
lock2 = RLock ()
machine = LockedMachine ( states = states , initial = 'A' , machine_context = [ lock1 , lock2 ])
Konteks apa pun melalui machine_model
akan dibagikan di antara semua model yang terdaftar dengan Machine
. Konteks per-model juga dapat ditambahkan:
lock3 = RLock ()
machine . add_model ( model , model_context = lock3 )
Penting bahwa semua manajer konteks yang disediakan pengguna masuk kembali karena mesin negara akan memanggil mereka beberapa kali, bahkan dalam konteks satu pemicu pemicu tunggal.
Jika Anda menggunakan Python 3.7 atau lebih baru, Anda dapat menggunakan AsyncMachine
untuk bekerja dengan panggilan balik asinkron. Anda dapat mencampur panggilan balik sinkron dan asinkron jika Anda suka tetapi ini mungkin memiliki efek samping yang tidak diinginkan. Perhatikan bahwa peristiwa perlu ditunggu dan loop acara juga harus ditangani oleh Anda.
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 ()
Jadi, mengapa Anda perlu menggunakan Python 3.7 atau lebih baru Anda mungkin bertanya. Dukungan Async telah diperkenalkan sebelumnya. AsyncMachine
memanfaatkan contextvars
untuk menangani panggilan balik berlari ketika acara baru tiba sebelum transisi selesai:
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
Contoh ini sebenarnya menggambarkan dua hal: pertama, 'pergi' yang disebut dalam transisi M1 dari A
ke B
tidak dibatalkan dan kedua, memanggil m2.fix()
akan menghentikan upaya transisi M2 dari A
ke B
dengan mengeksekusi 'perbaikan' dari A
ke C
. Pemisahan ini tidak akan mungkin terjadi tanpa contextvars
. Perhatikan bahwa prepare
dan conditions
tidak diperlakukan sebagai transisi yang berkelanjutan. Ini berarti bahwa setelah conditions
dievaluasi, transisi dieksekusi meskipun peristiwa lain sudah terjadi. Tugas hanya akan dibatalkan saat dijalankan sebagai panggilan balik before
atau lebih baru.
AsyncMachine
menampilkan mode antrian model khusus yang dapat digunakan saat queued='model'
diteruskan ke konstruktor. Dengan antrian khusus model, peristiwa hanya akan antri ketika mereka termasuk model yang sama. Selain itu, pengecualian yang dinaikkan hanya akan menghapus antrian acara dari model yang mengangkat pengecualian itu. Demi kesederhanaan, mari kita asumsikan bahwa setiap peristiwa di asyncio.gather
di bawah ini tidak dipicu pada saat yang sama tetapi sedikit tertunda:
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
Perhatikan bahwa mode antrian tidak boleh diubah setelah konstruksi mesin.
Jika pahlawan super Anda membutuhkan perilaku khusus, Anda dapat melempar fungsi tambahan dengan dekorasi mesin negara:
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
Saat ini, transisi dilengkapi dengan fitur keadaan berikut:
Timeout - memicu acara setelah beberapa waktu telah berlalu
timeout
(int, opsional) - jika lulus, keadaan yang dimasukkan akan batas waktu setelah timeout
detikon_timeout
(string/callable, opsional) - akan dipanggil saat waktu batas waktu telah tercapaiAttributeError
saat timeout
diatur tetapi on_timeout
tidakTag - menambahkan tag ke negara bagian
tags
(daftar, opsional) - Menetapkan tag ke negara bagianState.is_<tag_name>
akan mengembalikan True
ketika status telah ditandai dengan tag_name
, else False
Kesalahan - Meningkatkan MachineError
saat keadaan tidak dapat dibiarkan
Tags
(jika Anda menggunakan Error
jangan gunakan Tags
)accepted
(bool, opsional) - menandai keadaan yang diterimatags
kata kunci dapat dilewati, berisi 'diterima'auto_transitions
telah diatur ke False
. Kalau tidak, setiap negara bagian dapat keluar dengan metode to_<state>
.Volatile - menginisialisasi objek setiap kali suatu keadaan dimasukkan
volatile
(kelas, opsional) - Setiap kali status dimasukkan objek kelas tipe akan ditetapkan untuk model. Nama atribut didefinisikan oleh hook
. Jika dihilangkan, volatileObject kosong akan dibuat sebagai gantinyahook
(string, default = 'scope') - Nama atribut model untuk objek temporal. Anda dapat menulis ekstensi State
Anda sendiri dan menambahkannya dengan cara yang sama. Cukup perhatikan bahwa add_state_features
mengharapkan mixins . Ini berarti ekstensi Anda harus selalu memanggil metode yang ditimpa __init__
, enter
dan exit
. Perpanjangan Anda mungkin mewarisi dari negara tetapi juga akan bekerja tanpanya. Menggunakan @add_state_features
memiliki kelemahan yaitu mesin yang dihiasi tidak dapat diasamkan (lebih tepatnya, CustomState
yang dihasilkan secara dinamis tidak dapat diasamkan). Ini mungkin menjadi alasan untuk menulis kelas negara kustom khusus sebagai gantinya. Bergantung pada mesin negara yang dipilih, kelas negara bagian kustom Anda mungkin perlu menyediakan fitur -fitur negara tertentu. Misalnya, HierarchicalMachine
mengharuskan keadaan adat Anda untuk menjadi contoh NestedState
( State
tidak cukup). Untuk menyuntikkan status Anda, Anda dapat menetapkannya ke atribut kelas Machine
Anda state_cls
atau override Machine.create_state
jika Anda memerlukan beberapa prosedur spesifik yang dilakukan setiap kali suatu negara dibuat:
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 )
Jika Anda ingin menghindari utas di AsyncMachine
Anda sepenuhnya, Anda dapat mengganti fitur status Timeout
dengan AsyncTimeout
dari ekstensi asyncio
:
import asyncio
from transitions . extensions . states import add_state_features
from transitions . extensions . asyncio import AsyncTimeout , AsyncMachine
@ add_state_features ( AsyncTimeout )
class TimeoutMachine ( AsyncMachine ):
pass
states = [ 'A' , { 'name' : 'B' , 'timeout' : 0.2 , 'on_timeout' : 'to_C' }, 'C' ]
m = TimeoutMachine ( states = states , initial = 'A' , queued = True ) # see remark below
asyncio . run ( asyncio . wait ([ m . to_B (), asyncio . sleep ( 0.1 )]))
assert m . is_B () # timeout shouldn't be triggered
asyncio . run ( asyncio . wait ([ m . to_B (), asyncio . sleep ( 0.3 )]))
assert m . is_C () # now timeout should have been processed
Anda harus mempertimbangkan lulus queued=True
untuk konstruktor TimeoutMachine
. Ini akan memastikan bahwa peristiwa diproses secara berurutan dan menghindari kondisi balap asinkron yang mungkin muncul ketika waktu habis dan peristiwa terjadi di dekatnya.
Anda dapat melihat FAQ untuk beberapa inspirasi atau checkout django-transitions
. Ini telah dikembangkan oleh Christian Ledermann dan juga diselenggarakan di GitHub. Dokumentasi berisi beberapa contoh penggunaan.
Pertama, selamat! Anda mencapai akhir dokumentasi! Jika Anda ingin mencoba transitions
sebelum menginstalnya, Anda dapat melakukannya di buku catatan Jupyter interaktif di mybinder.org. Cukup klik tombol ini.
Untuk laporan bug dan masalah lainnya, buka masalah di GitHub.
Untuk pertanyaan penggunaan, posting di stack overflow, pastikan untuk menandai pertanyaan Anda dengan tag pytransitions
. Jangan lupa untuk melihat contoh yang diperluas!
Untuk pertanyaan lain, permohonan, atau hadiah moneter besar yang tidak dibatasi, email tal yarkoni (penulis awal) dan/atau Alexander Neumann (pemelihara saat ini).