การใช้งานเครื่องสถานะเชิงวัตถุแบบน้ำหนักเบาใน Python พร้อมส่วนขยายมากมาย เข้ากันได้กับ Python 2.7+ และ 3.0+
pip install transitions
... หรือโคลน repo จาก GitHub จากนั้น:
python setup.py install
พวกเขากล่าวว่าตัวอย่างที่ดีมีค่าเท่ากับเอกสาร API 100 หน้า หนึ่งล้านคำสั่ง หรือหนึ่งพันคำ
"พวกเขา" อาจจะโกหก... แต่นี่คือตัวอย่างต่อไป:
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?" )
ตอนนี้คุณได้อบสเตทแมชชีนลงใน NarcolepticSuperhero
แล้ว พาเขา/เธอ/มันออกไปปั่นกันเถอะ...
> >> 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
แม้ว่าเราจะอ่านใจแบทแมนตัวจริงไม่ได้ แต่เราก็สามารถเห็นภาพสถานะปัจจุบันของ NarcolepticSuperhero
ได้อย่างแน่นอน
ดูส่วนขยาย Diagrams หากคุณต้องการทราบวิธีการ
เครื่องสถานะเป็น แบบจำลอง พฤติกรรมที่ประกอบด้วย สถานะ จำนวนจำกัดและ การเปลี่ยนผ่าน ระหว่างสถานะเหล่านั้น ภายในแต่ละรัฐและการเปลี่ยนแปลงบาง อย่าง สามารถดำเนินการได้ เครื่องสถานะจำเป็นต้องเริ่มต้นที่ สถานะเริ่มต้น บางอย่าง เมื่อใช้ transitions
เครื่องสถานะอาจประกอบด้วยออบเจ็กต์หลายตัว โดยที่ ( เครื่องจักร ) บางตัวมีคำจำกัดความสำหรับการจัดการกับ ( โมเดล ) อื่น ๆ ด้านล่างนี้ เราจะดูแนวคิดหลักบางประการและวิธีการทำงานร่วมกับแนวคิดเหล่านั้น
สถานะ . สถานะแสดงถึงเงื่อนไขหรือขั้นตอนเฉพาะในเครื่องสถานะ เป็นรูปแบบพฤติกรรมหรือขั้นตอนที่ชัดเจนในกระบวนการ
การเปลี่ยนแปลง นี่คือกระบวนการหรือเหตุการณ์ที่ทำให้เครื่องสถานะเปลี่ยนจากสถานะหนึ่งไปอีกสถานะหนึ่ง
แบบอย่าง . โครงสร้างสถานะที่แท้จริง เป็นเอนทิตีที่ได้รับการอัปเดตระหว่างการเปลี่ยนภาพ นอกจากนี้ยังอาจกำหนดการ กระทำ ที่จะดำเนินการระหว่างการเปลี่ยนภาพ ตัวอย่างเช่น ก่อนการเปลี่ยนแปลงหรือเมื่อมีการเข้าหรือออกจากสถานะ
เครื่องจักร . นี่คือเอนทิตีที่จัดการและควบคุมแบบจำลอง สถานะ การเปลี่ยนผ่าน และการดำเนินการ เป็นตัวนำที่ควบคุมกระบวนการทั้งหมดของเครื่องสถานะ
สิ่งกระตุ้น . นี่คือเหตุการณ์ที่เริ่มต้นการเปลี่ยนแปลง ซึ่งเป็นวิธีการที่ส่งสัญญาณเพื่อเริ่มการเปลี่ยนแปลง
การกระทำ . การดำเนินการหรืองานเฉพาะที่ดำเนินการเมื่อมีการเข้า ออกจากสถานะ หรือระหว่างการเปลี่ยนแปลง การดำเนินการจะดำเนินการผ่าน การเรียกกลับ ซึ่งเป็นฟังก์ชันที่จะดำเนินการเมื่อมีเหตุการณ์บางอย่างเกิดขึ้น
การทำให้เครื่องสถานะพร้อมใช้งานนั้นค่อนข้างง่าย สมมติว่าคุณมี object lump
(อินสแตนซ์ของคลาส Matter
) และคุณต้องการจัดการสถานะของมัน:
class Matter ( object ):
pass
lump = Matter ()
คุณสามารถเริ่มต้นเครื่องสถานะการทำงาน ( น้อยที่สุด ) ที่เชื่อมโยงกับ lump
โมเดลดังนี้:
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'
อีกทางเลือกหนึ่งคือการไม่ส่งโมเดลไปยังเครื่องมือเริ่มต้น Machine
อย่างชัดเจน:
machine = Machine ( states = [ 'solid' , 'liquid' , 'gas' , 'plasma' ], initial = 'solid' )
# The machine instance itself now acts as a model
machine . state
> >> 'solid'
โปรดทราบว่าครั้งนี้ฉันไม่ได้ส่งโมเดล lump
เป็นข้อโต้แย้ง อาร์กิวเมนต์แรกที่ส่งผ่านไปยัง Machine
ทำหน้าที่เป็นแบบจำลอง ดังนั้นเมื่อฉันผ่านบางสิ่งไปที่นั่น ฟังก์ชั่นอำนวยความสะดวกทั้งหมดจะถูกเพิ่มให้กับออบเจ็กต์ หากไม่มีการระบุโมเดล อินสแตนซ์ machine
จะทำหน้าที่เป็นโมเดล
เมื่อแรกเริ่มฉันพูดว่า "ขั้นต่ำ" เนื่องจากในขณะที่เครื่องสถานะนี้ทำงานทางเทคนิค แต่ก็ไม่ได้ ทำ อะไรเลย มันเริ่มต้นในสถานะ 'solid'
แต่จะไม่มีวันย้ายไปสู่สถานะอื่น เนื่องจากยังไม่มีการกำหนดการเปลี่ยนผ่าน...!
ลองอีกครั้ง
# 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'
สังเกตเห็นวิธีการใหม่เอี่ยมที่แนบมากับอินสแตนซ์ Matter
( evaporate()
, ionize()
ฯลฯ ) แต่ละวิธีจะทำให้เกิดการเปลี่ยนแปลงที่สอดคล้องกัน การเปลี่ยนภาพยังสามารถถูกทริกเกอร์ แบบไดนามิกได้ โดยการเรียกเมธอด trigger()
ที่ให้มาพร้อมกับชื่อของการเปลี่ยนแปลง ดังที่แสดงไว้ด้านบน ข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ในส่วนการทริกเกอร์การเปลี่ยนแปลง
จิตวิญญาณของกลไกสถานะที่ดีใดๆ (และของกลไกที่ไม่ดีอีกมากมายอย่างไม่ต้องสงสัย) คือกลุ่มของรัฐ ข้างต้น เราได้กำหนดสถานะของโมเดลที่ถูกต้องโดยส่งรายการสตริงไปยังเครื่องมือเริ่มต้น Machine
แต่ภายในแล้ว รัฐจะแสดงเป็นวัตถุ State
จริงๆ
คุณสามารถเริ่มต้นและแก้ไขสถานะได้หลายวิธี โดยเฉพาะ คุณสามารถ:
Machine
เริ่มต้นโดยระบุชื่อของรัฐหรือState
ใหม่โดยตรงหรือตัวอย่างต่อไปนี้แสดงให้เห็นหลายวิธีในการบรรลุเป้าหมายเดียวกัน:
# 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 ])
สถานะจะเริ่มต้นได้ หนึ่งครั้ง เมื่อเพิ่มลงในเครื่อง และจะคงอยู่จนกว่าจะถูกลบออกจากเครื่อง กล่าวอีกนัยหนึ่ง: หากคุณแก้ไขคุณลักษณะของวัตถุสถานะ การเปลี่ยนแปลงนี้จะไม่ถูกรีเซ็ตในครั้งต่อไปที่คุณเข้าสู่สถานะนั้น ดูวิธีขยายฟีเจอร์สถานะในกรณีที่คุณต้องการพฤติกรรมอื่น
แต่การมีสถานะและสามารถเคลื่อนที่ไปมาระหว่างสถานะเหล่านั้นได้ (ช่วงการเปลี่ยนภาพ) ไม่ได้มีประโยชน์มากนักในตัวเอง จะเป็นอย่างไรถ้าคุณต้องการทำอะไรสักอย่าง ให้ดำเนิน การ บางอย่างเมื่อคุณเข้าหรือออกจากรัฐ? นี่คือที่มา ของการโทรกลับ
State
ยังสามารถเชื่อมโยงกับรายการการเรียกกลับ enter
และ exit
ซึ่งจะถูกเรียกเมื่อใดก็ตามที่เครื่องสถานะเข้าหรือออกจากสถานะนั้น คุณสามารถระบุการเรียกกลับในระหว่างการเตรียมใช้งานโดยส่งต่อไปยังตัวสร้างวัตถุ State
ในพจนานุกรมคุณสมบัติของรัฐหรือเพิ่มในภายหลัง
เพื่อความสะดวก เมื่อใดก็ตามที่มีการเพิ่ม State
ใหม่ลงใน Machine
วิธีการ on_enter_«state name»
และ on_exit_«state name»
จะถูกสร้างขึ้นแบบไดนามิกบน Machine (ไม่ใช่ในโมเดล!) ซึ่งช่วยให้คุณสามารถเพิ่มการเข้าและออกใหม่แบบไดนามิก โทรกลับในภายหลังหากคุณต้องการ
# 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!'
โปรดทราบว่าการโทรกลับ on_enter_«state name»
จะ ไม่ เริ่มทำงานเมื่อมีการเตรียมใช้งานเครื่องครั้งแรก ตัวอย่างเช่น หากคุณมีการกำหนดการโทรกลับ on_enter_A()
และเตรียมใช้งาน Machine
ด้วย initial='A'
on_enter_A()
จะไม่เริ่มทำงานจนกว่าคุณจะเข้าสู่สถานะ A
ครั้งถัดไป (หากคุณต้องการให้แน่ใจว่า on_enter_A()
เริ่มทำงานเมื่อเริ่มต้น คุณสามารถสร้างสถานะเริ่มต้นจำลอง จากนั้นเรียก to_A()
ภายในเมธอด __init__
อย่างชัดเจน)
นอกเหนือจากการส่งผ่านการโทรกลับเมื่อเริ่มต้น State
หรือเพิ่มแบบไดนามิกแล้ว ยังสามารถกำหนดการเรียกกลับในคลาสโมเดลได้ด้วย ซึ่งอาจเพิ่มความชัดเจนของโค้ด ตัวอย่างเช่น:
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' ])
ในตอนนี้ เมื่อใดก็ตามที่มีการเปลี่ยน lump
เป็นสถานะ A
เมธอด on_enter_A()
ที่กำหนดไว้ในคลาส Matter
จะเริ่มทำงาน
คุณสามารถใช้ on_final
callbacks ซึ่งจะถูกทริกเกอร์เมื่อมีการป้อนสถานะที่มี final=True
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 ...
คุณสามารถตรวจสอบสถานะปัจจุบันของโมเดลได้ตลอดเวลาโดย:
.state
หรือis_«state name»()
และหากคุณต้องการดึงข้อมูล State
object จริงสำหรับสถานะปัจจุบัน คุณสามารถทำได้โดยใช้เมธอด get_state()
ของอินสแตนซ์ Machine
lump . state
> >> 'solid'
lump . is_gas ()
> >> False
lump . is_solid ()
> >> True
machine . get_state ( lump . state ). name
> >> 'solid'
หากคุณต้องการคุณสามารถเลือกชื่อแอตทริบิวต์สถานะของคุณเองโดยส่งอาร์กิวเมนต์ model_attribute
ขณะเริ่มต้น Machine
สิ่งนี้จะเปลี่ยนชื่อของ is_«state name»()
เป็น is_«model_attribute»_«state name»()
ด้วย ในทำนองเดียวกัน การเปลี่ยนอัตโนมัติจะถูกตั้งชื่อเป็น to_«model_attribute»_«state name»()
แทนที่จะเป็น to_«state name»()
ซึ่งทำเพื่อให้เครื่องหลายเครื่องสามารถทำงานบนโมเดลเดียวกันโดยมีชื่อแอตทริบิวต์สถานะแต่ละรายการได้
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
จนถึงตอนนี้เราได้เห็นแล้วว่าเราสามารถตั้งชื่อรัฐและใช้ชื่อเหล่านี้เพื่อทำงานกับเครื่องสถานะของเราได้อย่างไร หากคุณชอบการพิมพ์ที่เข้มงวดกว่าและการเติมโค้ด IDE ให้สมบูรณ์มากขึ้น (หรือคุณไม่สามารถพิมพ์ 'sesquipedalophobia' ได้อีกต่อไปเพราะคำนั้นทำให้คุณกลัว) การใช้ Enumerations อาจเป็นสิ่งที่คุณกำลังมองหา:
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
คุณสามารถผสม enum และสตริงได้หากต้องการ (เช่น [States.RED, 'ORANGE', States.YELLOW, States.GREEN]
) แต่โปรดทราบว่าภายใน transitions
จะยังคงจัดการสถานะตามชื่อ ( enum.Enum.name
) ดังนั้นจึงเป็นไปไม่ได้ที่จะมีสถานะ 'GREEN'
และ States.GREEN
ในเวลาเดียวกัน
ตัวอย่างข้างต้นบางตัวอย่างได้แสดงให้เห็นการใช้ช่วงการเปลี่ยนภาพในการส่งผ่านแล้ว แต่เราจะมาดูรายละเอียดเพิ่มเติมในที่นี้
เช่นเดียวกับรัฐ แต่ละการเปลี่ยนแปลงจะถูกแสดงภายในเป็นวัตถุของตัวเอง - ตัวอย่างของคลาส Transition
วิธีที่เร็วที่สุดในการเริ่มต้นชุดการเปลี่ยนภาพคือการส่งพจนานุกรมหรือรายการพจนานุกรมไปยังเครื่องมือเริ่มต้น Machine
เราเห็นสิ่งนี้แล้วด้านบน:
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 )
การกำหนดช่วงการเปลี่ยนภาพในพจนานุกรมมีประโยชน์ในเรื่องความชัดเจน แต่ก็อาจยุ่งยากได้ หากคุณอยู่หลังเรื่องย่อ คุณอาจเลือกที่จะกำหนดช่วงการเปลี่ยนภาพโดยใช้รายการ เพียงตรวจสอบให้แน่ใจว่าองค์ประกอบในแต่ละรายการอยู่ในลำดับเดียวกันกับอาร์กิวเมนต์ตำแหน่งในการเริ่มต้น Transition
(เช่น trigger
source
destination
ฯลฯ)
รายการของรายการต่อไปนี้มีฟังก์ชันเทียบเท่ากับรายการพจนานุกรมด้านบน:
transitions = [
[ 'melt' , 'solid' , 'liquid' ],
[ 'evaporate' , 'liquid' , 'gas' ],
[ 'sublimate' , 'solid' , 'gas' ],
[ 'ionize' , 'gas' , 'plasma' ]
]
หรือคุณสามารถเพิ่มการเปลี่ยนภาพไปยัง Machine
ได้หลังจากการเริ่มต้น:
machine = Machine ( model = lump , states = states , initial = 'solid' )
machine . add_transition ( 'melt' , source = 'solid' , dest = 'liquid' )
เพื่อให้การเปลี่ยนแปลงดำเนินการได้ เหตุการณ์บางอย่างจำเป็นต้อง ทริกเกอร์ มีสองวิธีในการทำเช่นนี้:
โดยใช้วิธีการแนบอัตโนมัติในโมเดลพื้นฐาน:
> >> lump . melt ()
> >> lump . state
'liquid'
> >> lump . evaporate ()
> >> lump . state
'gas'
สังเกตว่าคุณไม่จำเป็นต้องกำหนดวิธีการเหล่านี้อย่างชัดเจนทุกที่ ชื่อของแต่ละการเปลี่ยนแปลงจะผูกไว้กับโมเดลที่ส่งผ่านไปยัง Machine
Initializer (ในกรณีนี้คือ lump
) นอกจากนี้ยังหมายความว่าโมเดลของคุณ ไม่ควร มีวิธีการที่มีชื่อเดียวกันกับทริกเกอร์เหตุการณ์อยู่แล้ว เนื่องจาก transitions
จะแนบวิธีการอำนวยความสะดวกเข้ากับโมเดลของคุณหากยังไม่ได้ใช้จุดนั้น หากคุณต้องการแก้ไขพฤติกรรมดังกล่าว โปรดดูที่คำถามที่พบบ่อย
ตอนนี้ใช้วิธี trigger
เพื่อแนบกับโมเดลของคุณแล้ว (หากยังไม่เคยมีมาก่อน) วิธีนี้ช่วยให้คุณดำเนินการเปลี่ยนผ่านตามชื่อในกรณีที่จำเป็นต้องมีการทริกเกอร์แบบไดนามิก:
> >> lump . trigger ( 'melt' )
> >> lump . state
'liquid'
> >> lump . trigger ( 'evaporate' )
> >> lump . state
'gas'
ตามค่าเริ่มต้น การทริกเกอร์การเปลี่ยนแปลงที่ไม่ถูกต้องจะทำให้เกิดข้อยกเว้น:
> >> 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!"
โดยทั่วไปแล้วพฤติกรรมนี้เป็นสิ่งที่พึงปรารถนา เนื่องจากจะช่วยแจ้งเตือนคุณเกี่ยวกับปัญหาในโค้ดของคุณ แต่ในบางกรณี คุณอาจต้องการเพิกเฉยต่อทริกเกอร์ที่ไม่ถูกต้อง คุณสามารถทำได้โดยการตั้งค่า ignore_invalid_triggers=True
(ทั้งแบบทีละรัฐหรือแบบทั่วโลกสำหรับทุกรัฐ):
> >> # 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 )
หากคุณต้องการทราบว่าช่วงการเปลี่ยนภาพใดที่ถูกต้องจากสถานะใดสถานะหนึ่ง คุณสามารถใช้ 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' ]
หากคุณติดตามเอกสารนี้ตั้งแต่ต้น คุณจะสังเกตเห็นว่าจริงๆ แล้ว get_triggers
ส่งคืนทริกเกอร์มากกว่าทริกเกอร์ที่กำหนดไว้อย่างชัดเจนที่แสดงด้านบน เช่น to_liquid
และอื่นๆ สิ่งเหล่านี้เรียกว่า auto-transitions
และเราจะแนะนำในหัวข้อถัดไป
นอกเหนือจากการเปลี่ยนแปลงใดๆ ที่เพิ่มไว้อย่างชัดเจนแล้ว วิธีการ to_«state»()
จะถูกสร้างขึ้นโดยอัตโนมัติทุกครั้งที่มีการเพิ่มสถานะลงในอินสแตนซ์ Machine
วิธีการนี้จะเปลี่ยนไปเป็นสถานะเป้าหมายไม่ว่าเครื่องจะอยู่ในสถานะใดก็ตาม:
lump . to_liquid ()
lump . state
> >> 'liquid'
lump . to_solid ()
lump . state
> >> 'solid'
หากต้องการ คุณสามารถปิดใช้งานลักษณะการทำงานนี้ได้โดยการตั้งค่า auto_transitions=False
ในเครื่องมือเริ่มต้น Machine
ทริกเกอร์ที่ระบุสามารถแนบไปกับการเปลี่ยนหลายรายการได้ ซึ่งบางช่วงอาจเริ่มต้นหรือสิ้นสุดในสถานะเดียวกันได้ ตัวอย่างเช่น:
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' )
ในกรณีนี้ การเรียก transmogrify()
จะตั้งค่าสถานะของโมเดลเป็น 'solid'
หากเป็น 'plasma'
ในปัจจุบัน และตั้งค่าเป็น 'plasma'
มิฉะนั้น (โปรดทราบว่าเฉพาะการเปลี่ยนแปลงที่ตรงกัน ครั้งแรก เท่านั้นที่จะดำเนินการ ดังนั้น การเปลี่ยนแปลงที่กำหนดไว้ในบรรทัดสุดท้ายด้านบนจะไม่ทำอะไรเลย)
คุณยังสามารถสร้างทริกเกอร์ที่ทำให้เกิดการเปลี่ยนจาก ทุก รัฐไปยังปลายทางหนึ่งๆ ได้โดยใช้ไวลด์การ์ด '*'
:
machine . add_transition ( 'to_liquid' , '*' , 'liquid' )
โปรดทราบว่าการเปลี่ยนไวด์การ์ดจะใช้กับสถานะที่มีอยู่ ณ เวลาที่เรียก add_transition() เท่านั้น การเรียกการเปลี่ยนแปลงแบบไวด์การ์ดเมื่อโมเดลอยู่ในสถานะที่เพิ่มหลังจากกำหนดการเปลี่ยนแปลงจะทำให้เกิดข้อความการเปลี่ยนแปลงที่ไม่ถูกต้อง และจะไม่เปลี่ยนเป็นสถานะเป้าหมาย
คุณสามารถเพิ่มทริกเกอร์สะท้อนกลับ (ทริกเกอร์ที่มีสถานะเดียวกันกับแหล่งที่มาและปลายทาง) ได้อย่างง่ายดายโดยระบุ =
เป็นปลายทาง สิ่งนี้มีประโยชน์หากควรเพิ่มทริกเกอร์สะท้อนกลับเดียวกันในหลายสถานะ ตัวอย่างเช่น:
machine . add_transition ( 'touch' , [ 'liquid' , 'gas' , 'plasma' ], '=' , after = 'change_shape' )
สิ่งนี้จะเพิ่มการเปลี่ยนภาพสะท้อนสำหรับทั้งสามสถานะด้วย touch()
เป็นทริกเกอร์ และดำเนินการ change_shape
หลังจากแต่ละทริกเกอร์
ตรงกันข้ามกับการเปลี่ยนผ่านแบบสะท้อนกลับ การเปลี่ยนผ่านภายในจะไม่มีวันออกจากสถานะจริงๆ ซึ่งหมายความว่าการโทรกลับที่เกี่ยวข้องกับการเปลี่ยนแปลง เช่น before
หรือ after
จะได้รับการประมวลผล ในขณะที่การโทรกลับที่เกี่ยวข้องกับสถานะจะไม่ exit
หรือ enter
หากต้องการกำหนดให้การเปลี่ยนเป็นแบบภายใน ให้ตั้งค่าปลายทางเป็น None
machine . add_transition ( 'internal' , [ 'liquid' , 'gas' ], None , after = 'change_shape' )
ความปรารถนาร่วมกันคือให้การเปลี่ยนสถานะเป็นไปตามลำดับเชิงเส้นที่เข้มงวด ตัวอย่างเช่น เมื่อระบุสถานะ ['A', 'B', 'C']
คุณอาจต้องการการเปลี่ยนที่ถูกต้องสำหรับ A
→ B
, B
→ C
และ C
→ A
(แต่ไม่มีคู่อื่น)
เพื่ออำนวยความสะดวกในการทำงานนี้ Transitions จึงจัดให้มีเมธอด add_ordered_transitions()
ในคลาส 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!"
การทำงานเริ่มต้นในการเปลี่ยนคือการประมวลผลเหตุการณ์ทันที ซึ่งหมายความว่าเหตุการณ์ภายในเมธอด on_enter
จะถูกประมวลผล ก่อนที่ จะเรียกการเรียกกลับที่ผูกไว้กับ after
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?
ลำดับการดำเนินการของตัวอย่างนี้คือ
prepare -> before -> on_enter_B -> on_enter_C -> after.
หากเปิดใช้งานการประมวลผลแบบเข้าคิว การเปลี่ยนแปลงจะเสร็จสิ้นก่อนที่จะทริกเกอร์การเปลี่ยนแปลงครั้งถัดไป:
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!
ซึ่งส่งผลให้
prepare -> before -> on_enter_B -> queue(to_C) -> after -> on_enter_C.
หมายเหตุสำคัญ: เมื่อประมวลผลเหตุการณ์ในคิว การเรียกใช้ทริกเกอร์จะส่งกลับ True
เสมอ เนื่องจากไม่มีวิธีใดที่จะระบุได้ในขณะเข้าคิวว่าการเปลี่ยนแปลงที่เกี่ยวข้องกับการโทรในคิวจะเสร็จสมบูรณ์ในที่สุดหรือไม่ สิ่งนี้จะเกิดขึ้นจริงแม้ว่าจะมีการประมวลผลเพียงเหตุการณ์เดียวก็ตาม
machine . add_transition ( 'jump' , 'A' , 'C' , conditions = 'will_fail' )
...
# queued=False
machine . jump ()
> >> False
# queued=True
machine . jump ()
> >> True
เมื่อโมเดลถูกลบออกจากเครื่อง transitions
จะลบเหตุการณ์ที่เกี่ยวข้องทั้งหมดออกจากคิวด้วย
class Model :
def on_enter_B ( self ):
self . to_C () # add event to queue ...
self . machine . remove_model ( self ) # aaaand it's gone
บางครั้งคุณเพียงต้องการให้ดำเนินการเฉพาะการเปลี่ยนแปลงหากมีเงื่อนไขเฉพาะเกิดขึ้น คุณสามารถทำได้โดยส่งวิธีการหรือรายการวิธีการใน 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' ])
ในตัวอย่างข้างต้น การเรียก heat()
เมื่อโมเดลอยู่ในสถานะ 'solid'
จะเปลี่ยนเป็นสถานะ 'gas'
หาก is_flammable
ส่งคืน True
มิฉะนั้น มันจะเปลี่ยนเป็นสถานะ 'liquid'
หาก is_really_hot
ส่งคืน True
เพื่อความสะดวก ยังมีอาร์กิวเมนต์ 'unless'
ที่ทำงานเหมือนกับเงื่อนไขทุกประการ แต่กลับด้าน:
machine . add_transition ( 'heat' , 'solid' , 'gas' , unless = [ 'is_flammable' , 'is_really_hot' ])
ในกรณีนี้ โมเดลจะเปลี่ยนจากสถานะของแข็งเป็นแก๊สทุกครั้งที่เกิด heat()
โดยมีเงื่อนไขว่าทั้ง is_flammable()
และ is_really_hot()
ส่งคืน False
โปรดทราบว่าวิธีการตรวจสอบเงื่อนไขจะได้รับอาร์กิวเมนต์เสริมและ/หรือออบเจ็กต์ข้อมูลที่ส่งผ่านไปยังวิธีการทริกเกอร์ ตัวอย่างเช่นการโทรต่อไปนี้:
lump . heat ( temp = 74 )
# equivalent to lump.trigger('heat', temp=74)
... จะส่งผ่าน temp=74
ตัวเลือก kwarg ไปยังการตรวจสอบ is_flammable()
(อาจรวมอยู่ในอินสแตนซ์ EventData
) หากต้องการข้อมูลเพิ่มเติม โปรดดูส่วนการส่งผ่านข้อมูลด้านล่าง
หากคุณต้องการให้แน่ใจว่าการเปลี่ยนแปลงเป็นไปได้ก่อนที่คุณจะดำเนินการต่อ คุณสามารถใช้ฟังก์ชัน may_<trigger_name>
ที่ได้เพิ่มลงในโมเดลของคุณ โมเดลของคุณยังมีฟังก์ชัน may_trigger
เพื่อตรวจสอบทริกเกอร์ตามชื่อ:
# check if the current temperature is hot enough to trigger a transition
if lump . may_heat ():
# if lump.may_trigger("heat"):
lump . heat ()
สิ่งนี้จะดำเนินการ prepare
การโทรกลับทั้งหมดและประเมินเงื่อนไขที่กำหนดให้กับการเปลี่ยนแปลงที่อาจเกิดขึ้น การตรวจสอบการเปลี่ยนยังสามารถใช้ได้เมื่อปลายทางของการเปลี่ยนไม่พร้อมใช้งาน (ยัง):
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
คุณสามารถแนบการโทรกลับไปยังช่วงการเปลี่ยนภาพและสถานะได้ การเปลี่ยนแปลงทุกครั้งจะมีแอตทริบิวต์ 'before'
และ 'after'
ซึ่งมีรายการวิธีการเรียกก่อนและหลังการดำเนินการการเปลี่ยนแปลง:
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?"
นอกจากนี้ยังมีการเรียกกลับ 'prepare'
ที่จะดำเนินการทันทีที่การเปลี่ยนแปลงเริ่มต้น ก่อนที่จะตรวจสอบ 'conditions'
ใด ๆ หรือดำเนินการเรียกกลับอื่น ๆ
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!"
โปรดทราบว่า prepare
จะไม่ถูกเรียก เว้นแต่สถานะปัจจุบันเป็นแหล่งที่มาที่ถูกต้องสำหรับการเปลี่ยนแปลงที่มีชื่อ
การดำเนินการเริ่มต้นที่จะดำเนินการก่อนหรือหลังการเปลี่ยนแปลง ทุกครั้ง สามารถส่งผ่านไปยัง Machine
ในระหว่างการเริ่มต้นด้วย before_state_change
และ 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?"
นอกจากนี้ยังมีคำหลักสองคำสำหรับการโทรกลับซึ่งควรดำเนินการ แยกกัน a) จำนวนการเปลี่ยนที่เป็นไปได้ b) หากการเปลี่ยนแปลงใด ๆ สำเร็จ และ c) แม้ว่าจะมีข้อผิดพลาดเกิดขึ้นระหว่างการดำเนินการของการโทรกลับอื่น ๆ การโทรกลับที่ส่งผ่านไปยัง Machine
ด้วย prepare_event
จะถูกดำเนินการ หนึ่งครั้ง ก่อนที่จะประมวลผลการเปลี่ยนแปลงที่เป็นไปได้ (และการโทรกลับ prepare
แต่ละรายการ) จะเกิดขึ้น การเรียกกลับของ finalize_event
จะถูกดำเนินการโดยไม่คำนึงถึงความสำเร็จของการเปลี่ยนผ่านที่ประมวลผล โปรดทราบว่าหากมีข้อผิดพลาดเกิดขึ้น ข้อมูลดังกล่าวจะถูกแนบไปกับ event_data
เป็น error
และสามารถเรียกข้อมูลได้ด้วย 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
บางครั้งสิ่งต่างๆ ก็ไม่เป็นไปตามที่ตั้งใจไว้ และเราจำเป็นต้องจัดการกับข้อยกเว้นและทำความสะอาดเพื่อให้ทุกอย่างดำเนินต่อไป เราสามารถส่งการโทรกลับไปที่ on_exception
เพื่อทำสิ่งนี้:
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
ดังที่คุณคงทราบแล้วว่าวิธีมาตรฐานในการส่ง callables ไปยังสถานะ เงื่อนไข และการเปลี่ยนผ่านนั้นเป็นไปตามชื่อ เมื่อประมวลผลการโทรกลับและเงื่อนไข transitions
จะใช้ชื่อเพื่อดึงข้อมูลที่สามารถเรียกได้ที่เกี่ยวข้องจากโมเดล หากไม่สามารถดึงข้อมูลเมธอดได้และมีจุด transitions
จะถือว่าชื่อเป็นเส้นทางไปยังฟังก์ชันโมดูลและพยายามนำเข้า หรือคุณสามารถส่งผ่านชื่อของคุณสมบัติหรือแอตทริบิวต์ได้ พวกมันจะถูกรวมเข้ากับฟังก์ชันต่างๆ แต่ไม่สามารถรับข้อมูลเหตุการณ์ได้ด้วยเหตุผลที่ชัดเจน คุณยังสามารถส่งผ่าน callables เช่น ฟังก์ชัน (ผูกมัด) ได้โดยตรง ตามที่กล่าวไว้ข้างต้น คุณยังสามารถส่งรายการ/ทูเพิลของชื่อที่เรียกได้ไปยังพารามิเตอร์การเรียกกลับได้ การโทรกลับจะดำเนินการตามลำดับที่ถูกเพิ่ม
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 ()
ความละเอียดที่เรียกได้เสร็จสิ้นใน Machine.resolve_callable
วิธีนี้สามารถแทนที่ได้ในกรณีที่จำเป็นต้องใช้กลยุทธ์การแก้ปัญหาที่สามารถเรียกได้ที่ซับซ้อนมากขึ้น
ตัวอย่าง
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 )
โดยสรุป ขณะนี้มีสามวิธีในการทริกเกอร์เหตุการณ์ คุณสามารถเรียกใช้ฟังก์ชันอำนวยความสะดวกของโมเดล เช่น lump.melt()
เรียกใช้งานทริกเกอร์ด้วยชื่อ เช่น lump.trigger("melt")
หรือส่งเหตุการณ์ในหลายรุ่นด้วย machine.dispatch("melt")
(ดูหัวข้อเกี่ยวกับหลายรุ่นใน รูปแบบการเริ่มต้นทางเลือก) การเรียกกลับในช่วงการเปลี่ยนภาพจะดำเนินการตามลำดับต่อไปนี้:
โทรกลับ | สถานะปัจจุบัน | ความคิดเห็น |
---|---|---|
'machine.prepare_event' | source | ดำเนินการ หนึ่งครั้ง ก่อนที่จะประมวลผลการเปลี่ยนแต่ละครั้ง |
'transition.prepare' | source | ดำเนินการทันทีที่การเปลี่ยนแปลงเริ่มต้นขึ้น |
'transition.conditions' | source | เงื่อนไข อาจ ล้มเหลวและหยุดการเปลี่ยนแปลง |
'transition.unless' | source | เงื่อนไข อาจ ล้มเหลวและหยุดการเปลี่ยนแปลง |
'machine.before_state_change' | source | การโทรกลับเริ่มต้นที่ประกาศในโมเดล |
'transition.before' | source | |
'state.on_exit' | source | โทรกลับประกาศในสถานะต้นทาง |
<STATE CHANGE> | ||
'state.on_enter' | destination | โทรกลับประกาศในสถานะปลายทาง |
'transition.after' | destination | |
'machine.on_final' | destination | การโทรกลับกับเด็ก ๆ จะถูกเรียกก่อน |
'machine.after_state_change' | destination | การเรียกกลับเริ่มต้นที่ประกาศในโมเดล จะถูกเรียกหลังจากการเปลี่ยนภายในด้วย |
'machine.on_exception' | source/destination | การโทรกลับจะดำเนินการเมื่อมีการยกข้อยกเว้น |
'machine.finalize_event' | source/destination | การโทรกลับจะดำเนินการแม้ว่าจะไม่มีการเปลี่ยนแปลงเกิดขึ้นหรือมีข้อยกเว้นเกิดขึ้นก็ตาม |
หากการโทรกลับทำให้เกิดข้อยกเว้น การประมวลผลการโทรกลับจะไม่ดำเนินต่อไป ซึ่งหมายความว่าเมื่อมีข้อผิดพลาดเกิดขึ้นก่อนการเปลี่ยนแปลง (ใน state.on_exit
หรือก่อนหน้า) ข้อผิดพลาดนั้นจะหยุดลง ในกรณีที่มีการเพิ่มขึ้นหลังจากดำเนินการเปลี่ยนแปลงแล้ว (ใน state.on_enter
หรือใหม่กว่า) การเปลี่ยนแปลงสถานะจะยังคงอยู่และไม่มีการย้อนกลับเกิดขึ้น การเรียกกลับที่ระบุใน machine.finalize_event
จะถูกดำเนินการเสมอ เว้นแต่จะมีข้อยกเว้นเกิดขึ้นโดยการสรุปการเรียกกลับเอง โปรดทราบว่าแต่ละลำดับการโทรกลับจะต้องเสร็จสิ้นก่อนที่จะดำเนินการขั้นตอนถัดไป การบล็อกการโทรกลับจะหยุดลำดับการดำเนินการ และด้วยเหตุนี้จึงบล็อกทริก trigger
หรือการ dispatch
เอง หากคุณต้องการให้ดำเนินการเรียกกลับแบบคู่ขนาน คุณสามารถดูส่วนขยาย AsyncMachine
สำหรับการประมวลผลแบบอะซิงโครนัสหรือ LockedMachine
สำหรับเธรด
บางครั้งคุณจำเป็นต้องส่งฟังก์ชันการโทรกลับที่ลงทะเบียนไว้เมื่อเริ่มต้นเครื่องด้วยข้อมูลบางอย่างที่สะท้อนถึงสถานะปัจจุบันของโมเดล การเปลี่ยนช่วยให้คุณทำสิ่งนี้ได้สองวิธี
ขั้นแรก (ค่าเริ่มต้น) คุณสามารถส่งผ่านข้อโต้แย้งเกี่ยวกับตำแหน่งหรือคำหลักไปยังวิธีการทริกเกอร์ได้โดยตรง (สร้างขึ้นเมื่อคุณเรียก 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.'
คุณสามารถส่งอาร์กิวเมนต์ที่คุณต้องการไปยังทริกเกอร์จำนวนเท่าใดก็ได้
มีข้อจำกัดที่สำคัญประการหนึ่งสำหรับแนวทางนี้ คือ ทุกฟังก์ชันการเรียกกลับที่ถูกกระตุ้นโดยการเปลี่ยนสถานะจะต้องสามารถจัดการกับข้อโต้แย้ง ทั้งหมด ได้ นี่อาจทำให้เกิดปัญหาได้หากการเรียกกลับแต่ละครั้งคาดหวังข้อมูลที่แตกต่างกันบ้าง
เพื่อแก้ไขปัญหานี้ Transitions สนับสนุนวิธีอื่นในการส่งข้อมูล หากคุณตั้ง send_event=True
ในการเริ่มต้น Machine
ทั้งหมดที่ทริกเกอร์จะถูกรวมไว้ในอินสแตนซ์ EventData
และส่งต่อไปยังการโทรกลับทุกครั้ง (ออบเจ็กต์ EventData
ยังรักษาการอ้างอิงภายในไปยังสถานะต้นทาง โมเดล การเปลี่ยนแปลง เครื่องจักร และทริกเกอร์ที่เกี่ยวข้องกับเหตุการณ์ ในกรณีที่คุณต้องการเข้าถึงข้อมูลเหล่านี้เพื่ออะไรก็ตาม)
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.'
ในตัวอย่างทั้งหมดจนถึงตอนนี้ เราได้แนบอินสแตนซ์ Machine
ใหม่เข้ากับโมเดลแยกต่างหาก ( lump
เป็นอินสแตนซ์ของคลาส Matter
) แม้ว่าการแยกนี้จะทำให้สิ่งต่าง ๆ เป็นระเบียบเรียบร้อย (เพราะคุณไม่จำเป็นต้องแพตช์วิธีการใหม่ ๆ มากมายในคลาส Matter
) มันก็อาจสร้างความรำคาญได้เช่นกัน เนื่องจากคุณต้องติดตามว่าวิธีการใดที่ถูกเรียกใช้บนเครื่องสถานะ และอันไหนที่ถูกเรียกใช้บนโมเดลที่เครื่องสถานะถูกผูกไว้ (เช่น lump.on_enter_StateA()
กับ machine.add_transition()
)
โชคดีที่ Transitions มีความยืดหยุ่น และรองรับรูปแบบการเริ่มต้นอื่นๆ สองรูปแบบ
ขั้นแรก คุณสามารถสร้างเครื่องสถานะแบบสแตนด์อโลนที่ไม่ต้องใช้โมเดลอื่นเลย เพียงละเว้นข้อโต้แย้ง model ในระหว่างการเริ่มต้น:
machine = Machine ( states = states , transitions = transitions , initial = 'solid' )
machine . melt ()
machine . state
> >> 'liquid'
หากคุณเริ่มต้นเครื่องด้วยวิธีนี้ คุณจะสามารถแนบเหตุการณ์ที่กระตุ้นทั้งหมด (เช่น evaporate()
, sublimate()
ฯลฯ) และฟังก์ชันการโทรกลับทั้งหมดไปที่อินสแตนซ์ Machine
ได้โดยตรง
วิธีการนี้มีประโยชน์ในการรวมฟังก์ชันการทำงานของเครื่องสถานะทั้งหมดไว้ในที่เดียว แต่อาจรู้สึกไม่เป็นธรรมชาติเล็กน้อยหากคุณคิดว่าตรรกะของสถานะควรมีอยู่ในตัวโมเดลเอง แทนที่จะอยู่ในตัวควบคุมที่แยกต่างหาก
อีกวิธีหนึ่ง (อาจดีกว่า) คือให้โมเดลสืบทอดมาจากคลาส Machine
การเปลี่ยนผ่านได้รับการออกแบบมาเพื่อรองรับการสืบทอดได้อย่างราบรื่น (อย่าลืมแทนที่วิธี __init__
ของคลาส 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'
ที่นี่ คุณจะได้รวมฟังก์ชันการทำงานของเครื่องสถานะทั้งหมดไว้ในโมเดลที่คุณมีอยู่ ซึ่งมักจะให้ความรู้สึกเป็นธรรมชาติมากกว่าการรวมฟังก์ชันทั้งหมดที่เราต้องการในอินสแตน Machine
แบบสแตนด์อโลนที่แยกต่างหาก
เครื่องจักรสามารถรองรับหลายรุ่นซึ่งสามารถส่งผ่านเป็นรายการได้ เช่น Machine(model=[model1, model2, ...])
ในกรณีที่คุณต้องการเพิ่มโมเดล และ อินสแตนซ์ของเครื่องเอง คุณสามารถส่งตัวยึดตัวแปรคลาส (สตริง) Machine.self_literal
ในระหว่างการเริ่มต้น เช่น Machine(model=[Machine.self_literal, model1, ...])
คุณยังสามารถสร้างเครื่องแบบสแตนด์อโลน และลงทะเบียนโมเดลแบบไดนามิกผ่าน machine.add_model
โดยส่ง model=None
ไปยังตัวสร้าง นอกจากนี้ คุณยังสามารถใช้ machine.dispatch
เพื่อทริกเกอร์เหตุการณ์ในรุ่นที่เพิ่มเข้ามาทั้งหมดในปัจจุบันได้ อย่าลืมเรียก machine.remove_model
หากเครื่องมีอายุการใช้งานยาวนาน และโมเดลของคุณเป็นแบบชั่วคราวและควรถูกรวบรวมขยะ:
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
หากคุณไม่ได้ระบุสถานะเริ่มต้นในตัวสร้างเครื่องสถานะ transitions
จะสร้างและเพิ่มสถานะเริ่มต้นที่เรียกว่า 'initial'
หากคุณไม่ต้องการสถานะเริ่มต้นเริ่มต้น คุณสามารถส่งผ่าน initial=None
อย่างไรก็ตาม ในกรณีนี้ คุณจะต้องผ่านสถานะเริ่มต้นทุกครั้งที่คุณเพิ่มโมเดล
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_attribute
ที่แตกต่างกัน ตามที่กล่าวไว้ในสถานะการตรวจสอบ สิ่งนี้จะเพิ่มฟังก์ชัน is/to_<model_attribute>_<state_name>
แบบกำหนดเอง:
lump = Matter ()
matter_machine = Machine ( lump , states = [ 'solid' , 'liquid' , 'gas' ], initial = 'solid' )
# add a second machine to the same model but assign a different state attribute
shipment_machine = Machine ( lump , states = [ 'delivered' , 'shipping' ], initial = 'delivered' , model_attribute = 'shipping_state' )
lump . state
> >> 'solid'
lump . is_solid () # check the default field
> >> True
lump . shipping_state
> >> 'delivered'
lump . is_shipping_state_delivered () # check the custom field.
> >> True
lump . to_shipping_state_shipping ()
> >> True
lump . is_shipping_state_delivered ()
> >> False
การเปลี่ยนผ่านมีความสามารถในการบันทึกขั้นพื้นฐานมาก เหตุการณ์จำนวนหนึ่ง ได้แก่ การเปลี่ยนแปลงสถานะ ทริกเกอร์การเปลี่ยนแปลง และการตรวจสอบตามเงื่อนไข จะถูกบันทึกเป็นเหตุการณ์ระดับ INFO โดยใช้โมดูล logging
Python มาตรฐาน ซึ่งหมายความว่าคุณสามารถกำหนดค่าการบันทึกเป็นเอาต์พุตมาตรฐานในสคริปต์ได้อย่างง่ายดาย:
# 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' )
...
เครื่องจักรสามารถดองได้และสามารถจัดเก็บและบรรจุด้วย pickle
ได้ สำหรับ Python 3.3 และ 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' ]
ดังที่คุณคงสังเกตเห็นแล้วว่า transitions
ใช้คุณสมบัติไดนามิกบางอย่างของ Python เพื่อให้คุณจัดการกับโมเดลได้อย่างสะดวก อย่างไรก็ตาม ตัวตรวจสอบประเภทคงที่ไม่ชอบแอตทริบิวต์ของโมเดลและวิธีการที่ไม่เป็นที่รู้จักก่อนรันไทม์ ในอดีต transitions
ไม่ได้กำหนดวิธีการอำนวยความสะดวกที่กำหนดไว้แล้วในแบบจำลองเพื่อป้องกันการแทนที่โดยไม่ตั้งใจ
แต่ไม่ต้องกังวล! คุณสามารถใช้พารามิเตอร์ตัวสร้างเครื่องจักร model_override
เพื่อเปลี่ยนวิธีการตกแต่งโมเดลได้ หากคุณตั้งค่า model_override=True
transitions
จะแทนที่วิธีการที่กำหนดไว้แล้วเท่านั้น วิธีนี้จะป้องกันไม่ให้วิธีการใหม่ๆ ปรากฏขึ้นขณะรันไทม์ และยังช่วยให้คุณกำหนดวิธีการช่วยเหลือที่คุณต้องการใช้ได้อีกด้วย
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"
หากคุณต้องการใช้ฟังก์ชันอำนวยความสะดวกทั้งหมดและรวมการเรียกกลับเข้าด้วยกัน การกำหนดโมเดลอาจค่อนข้างซับซ้อนเมื่อคุณกำหนดสถานะและการเปลี่ยนผ่านจำนวนมาก เมธอด generate_base_model
ในช่วง transitions
สามารถสร้างโมเดลพื้นฐานจากการกำหนดค่าเครื่องเพื่อช่วยคุณในเรื่องนี้
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 )
การกำหนดวิธีโมเดลที่จะถูกแทนที่จะเพิ่มงานพิเศษเล็กน้อย อาจยุ่งยากในการสลับไปมาเพื่อให้แน่ใจว่าชื่อเหตุการณ์สะกดถูกต้อง โดยเฉพาะอย่างยิ่งหากมีการกำหนดสถานะและการเปลี่ยนในรายการก่อนหรือหลังโมเดลของคุณ คุณสามารถลดรูปแบบสำเร็จรูปและความไม่แน่นอนในการทำงานกับสตริงได้โดยการกำหนดสถานะเป็นการแจงนับ คุณยังสามารถกำหนดการเปลี่ยนภาพได้ในคลาสโมเดลของคุณด้วยความช่วยเหลือของ add_transitions
และ event
ขึ้นอยู่กับคุณว่าคุณใช้ฟังก์ชันมัณฑนากร add_transitions
หรือเหตุการณ์เพื่อกำหนดค่าให้กับแอตทริบิวต์หรือไม่นั้นขึ้นอยู่กับสไตล์โค้ดที่คุณต้องการ ทั้งสองทำงานในลักษณะเดียวกัน มีลายเซ็นเหมือนกัน และควรให้ผลลัพธ์ (เกือบ) คำแนะนำประเภท IDE เดียวกัน เนื่องจากยังอยู่ในระหว่างดำเนินการ คุณจะต้องสร้างคลาส Machine แบบกำหนดเองและใช้ with_model_definitions สำหรับการเปลี่ยนเพื่อตรวจสอบการเปลี่ยนที่กำหนดในลักษณะนั้น
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
แม้ว่าแกนหลักของการเปลี่ยนภาพจะมีน้ำหนักเบา แต่ก็มี MixIns มากมายที่จะขยายฟังก์ชันการทำงาน รองรับในปัจจุบันคือ:
มีสองกลไกในการดึงข้อมูลอินสแตนซ์เครื่องสถานะพร้อมเปิดใช้งานคุณสมบัติที่ต้องการ แนวทางแรกใช้ประโยชน์จาก factory
อำนวยความสะดวกด้วย graph
พารามิเตอร์สี่ตัว , nested
, locked
หรือ asyncio
ที่ตั้งค่าเป็น True
หากจำเป็นต้องใช้คุณสมบัติ:
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 )
แนวทางนี้มุ่งเป้าไปที่การใช้งานเชิงทดลอง เนื่องจากในกรณีนี้ไม่จำเป็นต้องทราบคลาสพื้นฐาน อย่างไรก็ตาม คลาสยังสามารถนำเข้าได้โดยตรงจาก transitions.extensions
รูปแบบการตั้งชื่อมีดังนี้:
ไดอะแกรม | ซ้อนกัน | ล็อคแล้ว | อะซินซิโอ | |
---|---|---|---|---|
เครื่องจักร | ||||
กราฟแมชชีน | ||||
ลำดับชั้นMachine | ||||
ล็อคเครื่อง | ||||
ลำดับชั้นGraphMachine | ||||
ล็อกกราฟแมชชีน | ||||
ล็อก HierarchicalMachine | ||||
ล็อกลำดับชั้นGraphMachine | ||||
อะซิงก์แมชชีน | ||||
AsyncGraphMachine | ||||
AsyncMachine แบบลำดับชั้น | ||||
AsyncGraphMachine แบบลำดับชั้น |
หากต้องการใช้เครื่องสถานะที่มีคุณลักษณะหลากหลาย เราสามารถเขียนได้:
from transitions . extensions import LockedHierarchicalGraphMachine as LHGMachine
machine = LHGMachine ( model , states , transitions )
การเปลี่ยนผ่านมีโมดูลส่วนขยายที่อนุญาตให้มีสถานะการซ้อน สิ่งนี้ช่วยให้เราสามารถสร้างบริบทและจำลองกรณีที่สถานะเกี่ยวข้องกับงานย่อยบางอย่างในเครื่องสถานะ หากต้องการสร้างสถานะที่ซ้อนกัน ให้นำเข้า NestedState
จากช่วงการเปลี่ยนภาพ หรือใช้พจนานุกรมที่มี name
อาร์กิวเมนต์การเริ่มต้นและ children
อีกทางหนึ่ง สามารถใช้ initial
เพื่อกำหนดสถานะย่อยที่จะส่งต่อเมื่อป้อนสถานะที่ซ้อนกัน
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')
การกำหนดค่าที่ใช้ initial
อาจมีลักษณะดังนี้:
# ...
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' ]
]
# ...
คีย์เวิร์ด initial
ของ Constructor HierarchicalMachine
ยอมรับสถานะที่ซ้อนกัน (เช่น initial='caffeinated_running'
) และรายการสถานะซึ่งถือว่าเป็นสถานะคู่ขนาน (เช่น initial=['A', 'B']
) หรือสถานะปัจจุบันของ รุ่นอื่น ( initial=model.state
) ซึ่งควรเป็นหนึ่งในตัวเลือกที่กล่าวถึงก่อนหน้านี้อย่างมีประสิทธิภาพ โปรดทราบว่าเมื่อส่งผ่านสตริง transition
จะตรวจสอบสถานะเป้าหมายสำหรับสถานะย่อย initial
และใช้เป็นสถานะรายการ สิ่งนี้จะทำแบบวนซ้ำจนกว่ารัฐย่อยจะไม่กล่าวถึงสถานะเริ่มต้น รัฐคู่ขนานหรือสถานะที่ส่งผ่านเป็นรายการจะถูกใช้ 'ตามสภาพ' และจะไม่มีการประเมินเบื้องต้นเพิ่มเติม
โปรดทราบว่าวัตถุสถานะที่คุณสร้างขึ้นก่อนหน้านี้ จะต้องเป็น NestedState
หรือคลาสที่ได้รับมา คลาส State
มาตรฐานที่ใช้ในอินสแตนซ์ Machine
แบบธรรมดาขาดคุณสมบัติที่จำเป็นสำหรับการซ้อน
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!
บางสิ่งที่ต้องพิจารณาเมื่อทำงานกับสถานะที่ซ้อนกัน: ชื่อรัฐจะถูกต่อเข้า กับ NestedState.separator
ขณะนี้ตัวคั่นถูกตั้งค่าเป็นขีดล่าง ('_') และดังนั้นจึงทำงานคล้ายกับเครื่องพื้นฐาน ซึ่งหมายความว่า bar
รัฐย่อยจาก state foo
จะเป็นที่รู้จักโดย foo_bar
รัฐย่อย baz
of bar
จะเรียกว่า foo_bar_baz
และอื่นๆ เมื่อเข้าสู่รัฐย่อย ระบบจะเรียก enter
สำหรับรัฐหลักทั้งหมด เช่นเดียวกับการออกจากรัฐย่อย ประการที่สาม รัฐที่ซ้อนกันสามารถเขียนทับพฤติกรรมการเปลี่ยนแปลงของผู้ปกครองได้ หากไม่ทราบการเปลี่ยนแปลงในสถานะปัจจุบัน การเปลี่ยนแปลงนั้นจะถูกมอบหมายให้กับระดับบนสุด
ซึ่งหมายความว่าในการกำหนดค่ามาตรฐาน ชื่อรัฐใน HSM จะต้องไม่มีขีดล่าง สำหรับ transitions
เป็นไปไม่ได้ที่จะบอกได้ว่า machine.add_state('state_name')
ควรเพิ่มสถานะชื่อ state_name
หรือเพิ่ม name
รัฐย่อยให้กับ state state
อย่างไรก็ตามในบางกรณีก็ยังไม่เพียงพอ ตัวอย่างเช่น หากชื่อรัฐประกอบด้วยคำมากกว่าหนึ่งคำ และคุณต้องการ/จำเป็นต้องใช้ขีดล่างเพื่อแยกคำเหล่านั้นแทน CamelCase
เพื่อจัดการกับสิ่งนี้ คุณสามารถเปลี่ยนอักขระที่ใช้ในการแยกได้อย่างง่ายดาย คุณยังสามารถใช้อักขระยูนิโค้ดแฟนซีได้หากคุณใช้ Python 3 การตั้งค่าตัวคั่นเป็นอย่างอื่นที่ไม่ใช่ขีดล่างจะเปลี่ยนพฤติกรรมบางอย่าง (auto_transition และการตั้งค่าการโทรกลับ) แม้ว่า:
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')
แทนที่จะเรียกว่า to_C_3_a()
การเปลี่ยนอัตโนมัติ to_C.s3.a()
หากรัฐย่อยของคุณขึ้นต้นด้วยตัวเลข การเปลี่ยนภาพจะเพิ่มคำนำหน้า 's' ('3' กลายเป็น 's3') ให้กับการเปลี่ยนอัตโนมัติ FunctionWrapper
เพื่อให้สอดคล้องกับรูปแบบการตั้งชื่อคุณลักษณะของ Python หากไม่จำเป็นต้องมีการโต้ตอบให้เสร็จสิ้น คุณสามารถเรียก to('C↦3↦a')
ได้โดยตรง นอกจากนี้ on_enter/exit_<<state name>>
จะถูกแทนที่ด้วย on_enter/exit(state_name, callback)
การตรวจสอบของรัฐสามารถดำเนินการในลักษณะเดียวกันได้ แทนที่จะใช้ is_C_3_a()
คุณสามารถใช้ตัวแปร FunctionWrapper
is_C.s3.a()
ได้
หากต้องการตรวจสอบว่าสถานะปัจจุบันเป็นรัฐย่อยของสถานะใดรัฐหนึ่งหรือไม่ is_state
รองรับคำหลัก 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
คุณสามารถใช้การแจงนับใน HSM ได้เช่นกัน แต่โปรดจำไว้ว่า Enum
จะถูกเปรียบเทียบตามค่า หากคุณมีค่ามากกว่าหนึ่งครั้งในแผนผังสถานะ สถานะเหล่านั้นจะไม่สามารถแยกแยะได้
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
ได้รับการเขียนใหม่ตั้งแต่ต้นเพื่อรองรับสถานะคู่ขนานและการแยกสถานะที่ซ้อนกันได้ดีขึ้น สิ่งนี้เกี่ยวข้องกับการปรับแต่งบางอย่างตามคำติชมของชุมชน หากต้องการทราบแนวคิดเกี่ยวกับลำดับการประมวลผลและการกำหนดค่า โปรดดูตัวอย่างต่อไปนี้:
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
เมื่อใช้ parallel
แทน children
transitions
จะเข้าสู่ทุกสถานะของรายการที่ส่งผ่านพร้อมกัน รัฐย่อยใดที่จะป้อนถูกกำหนดโดย initial
ซึ่งควรชี้ไปที่รัฐย่อยโดยตรง เสมอ คุณลักษณะใหม่คือการกำหนดการเปลี่ยนผ่านเฉพาะที่โดยการส่งผ่านคำสำคัญ transitions
ในคำจำกัดความของรัฐ การเปลี่ยนแปลงที่กำหนดไว้ข้างต้น ['go', 'a', 'b']
ใช้ได้เฉพาะใน C_1
เท่านั้น แม้ว่าคุณจะสามารถอ้างอิงสถานะย่อยได้เหมือนกับที่ทำใน ['go', '2_z', '2_x']
คุณไม่สามารถอ้างอิงสถานะหลักได้โดยตรงในการเปลี่ยนที่กำหนดในเครื่อง เมื่อออกจากสถานะแม่แล้ว ลูกก็จะออกจากสถานะด้วย นอกเหนือจากลำดับการประมวลผลของการเปลี่ยนที่รู้จักจาก Machine
โดยที่การเปลี่ยนจะถูกพิจารณาตามลำดับที่เพิ่มเข้าไป HierarchicalMachine
ยังพิจารณาลำดับชั้นด้วย การเปลี่ยนที่กำหนดใน Substates จะได้รับการประเมินก่อน (เช่น C_1_a
ถูกทิ้งไว้ก่อน C_2_z
) และการเปลี่ยนที่กำหนดด้วย Wildcard *
Will (ตอนนี้) เพิ่มการเปลี่ยนผ่านไปยังสถานะรูท (ในตัวอย่างนี้ A
, B
, C
) เริ่มต้นด้วย 0.8.0 รัฐที่ซ้อน สามารถเพิ่มได้โดยตรงและจะออกการสร้างสถานะหลักในการบิน:
m = HierarchicalMachine ( states = [ 'A' ], initial = 'A' )
m . add_state ( 'B_1_a' )
m . to_B_1 ()
assert m . is_B ( allow_substates = True )
การทดลองใน 0.9.1: คุณสามารถใช้ประโยชน์จากการเรียกกลับ on_final
ได้ทั้งในสถานะหรือบน HSM เอง การเรียกกลับจะถูกเรียกใช้หากก) สถานะของตัวเองถูกแท็กด้วย final
และเพิ่งถูกป้อนหรือ b) ผู้แทนทั้งหมดได้รับการพิจารณาขั้นสุดท้ายและอย่างน้อยหนึ่งสารย่อยที่เพิ่งเข้าสู่สถานะสุดท้าย ในกรณีของ B) ผู้ปกครองทุกคนจะได้รับการพิจารณาขั้นสุดท้ายเช่นกันหากเงื่อนไข B) ถือเป็นจริงสำหรับพวกเขา สิ่งนี้อาจเป็นประโยชน์ในกรณีที่การประมวลผลเกิดขึ้นแบบขนานและ HSM หรือสถานะหลักของคุณควรได้รับการแจ้งเตือนเมื่อผู้ส่งสินค้าทั้งหมดมาถึงสถานะสุดท้าย:
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!
นอกจากคำสั่งทางความหมายแล้วรัฐที่ซ้อนกันนั้นมีประโยชน์มากหากคุณต้องการระบุเครื่องจักรของรัฐสำหรับงานเฉพาะและวางแผนที่จะนำกลับมาใช้ใหม่ ก่อน 0.8.0 HierarchicalMachine
จะไม่รวมอินสแตนซ์ของเครื่องด้วยตัวเอง แต่รัฐและการเปลี่ยนแปลงโดยการสร้างสำเนา อย่างไรก็ตามเนื่องจากอินสแตนซ์ของรัฐ 0.8.0 (Nested)State
เพิ่ง ถูกอ้างอิง ซึ่งหมายถึงการเปลี่ยนแปลงในการรวบรวมรัฐและเหตุการณ์ของเครื่องหนึ่งจะมีผลต่ออินสแตนซ์ของเครื่องอื่น แบบจำลองและสถานะของพวกเขาจะไม่ถูกแบ่งปัน โปรดทราบว่าเหตุการณ์และการเปลี่ยนแปลงจะถูกคัดลอกโดยการอ้างอิงและจะถูกแชร์โดยทั้งสองกรณีหากคุณไม่ได้ใช้คำหลัก remap
การเปลี่ยนแปลงนี้ทำขึ้นเพื่อให้สอดคล้องกับ Machine
ซึ่งใช้อินสแตน State
ที่ผ่านโดยอ้างอิง
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
หากมีการส่ง HierarchicalMachine
ด้วยคำหลัก children
สถานะเริ่มต้นของเครื่องนี้จะถูกกำหนดให้กับสถานะหลักใหม่ ในตัวอย่างข้างต้นเราเห็นว่าการเข้า counting
จะเข้าสู่ counting_1
หากนี่เป็นพฤติกรรมที่ไม่พึงประสงค์และเครื่องควรหยุดในสถานะหลักผู้ใช้สามารถผ่าน initial
เป็น False
เช่น {'name': 'counting', 'children': counter, 'initial': False}
บางครั้งคุณต้องการคอลเลกชันสถานะที่ฝังตัวดังกล่าวเพื่อ 'กลับ' ซึ่งหมายความว่าหลังจากเสร็จสิ้นแล้วควรออกจากการขนส่งไปยังหนึ่งในรัฐซูเปอร์ของคุณ เพื่อให้บรรลุพฤติกรรมนี้คุณสามารถแมปการเปลี่ยนแปลงของรัฐได้ ในตัวอย่างข้างต้นเราต้องการให้เคาน์เตอร์กลับมาหากรัฐ done
สิ้น สิ่งนี้ทำดังนี้:
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
ดังที่ได้กล่าวมาแล้วการใช้ remap
จะ คัดลอก เหตุการณ์และการเปลี่ยนแปลงเนื่องจากไม่สามารถใช้งานได้ในเครื่องรัฐดั้งเดิม หากเครื่องของรัฐที่นำกลับมาใช้ใหม่ไม่มีสถานะสุดท้ายคุณสามารถเพิ่มการเปลี่ยนผ่านด้วยตนเองได้ ถ้า 'เคาน์เตอร์' ไม่มีสถานะ 'เสร็จ' เราสามารถเพิ่ม ['done', 'counter_3', 'waiting']
เพื่อให้ได้พฤติกรรมเดียวกัน
ในกรณีที่คุณต้องการให้รัฐและช่วงการเปลี่ยนภาพถูกคัดลอกโดยค่ามากกว่าการอ้างอิง (ตัวอย่างเช่นหากคุณต้องการรักษาพฤติกรรมก่อน -0.8) คุณสามารถทำได้โดยการสร้าง NestedState
และกำหนดสำเนาของเหตุการณ์และสถานะของเครื่องจักร มัน.
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 ]
สำหรับเครื่องสถานะที่ซับซ้อนการแชร์การกำหนดค่ามากกว่าเครื่องจักรแบบอินสแตนซ์อาจเป็นไปได้มากขึ้น โดยเฉพาะอย่างยิ่งเนื่องจากเครื่องจักรแบบอินสแตนซ์จะต้องได้มาจาก HierarchicalMachine
การกำหนดค่าดังกล่าวสามารถจัดเก็บและโหลดได้อย่างง่ายดายผ่าน JSON หรือ YAML (ดูคำถามที่พบบ่อย) HierarchicalMachine
ช่วยให้การกำหนด substates กับ children
หรือ states
ของคำหลัก หากทั้งคู่มีอยู่ children
เท่านั้นที่จะได้รับการพิจารณา
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 ()
คำหลักเพิ่มเติม:
title
(ไม่บังคับ): ตั้งชื่อชื่อของภาพที่สร้างขึ้นshow_conditions
(ค่าเริ่มต้นเท็จ): แสดงเงื่อนไขที่ขอบการเปลี่ยนแปลงshow_auto_transitions
(ค่าเริ่มต้นเท็จ): แสดงการเปลี่ยนอัตโนมัติในกราฟshow_state_attributes
(ค่าเริ่มต้นเท็จ): แสดงการโทรกลับ (ป้อน, ออก), แท็กและหมดเวลาในกราฟการเปลี่ยนแปลงสามารถสร้างแผนภาพสถานะพื้นฐานที่แสดงการเปลี่ยนแปลงที่ถูกต้องทั้งหมดระหว่างรัฐ การสนับสนุนแผนภาพพื้นฐานสร้างคำจำกัดความของเครื่อง Mermaid State ซึ่งสามารถใช้กับตัวแก้ไขสดของ Mermaid ในไฟล์ Markdown ใน Gitlab หรือ GitHub และบริการเว็บอื่น ๆ ตัวอย่างเช่นรหัสนี้:
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!" )
สร้างแผนภาพนี้ (ตรวจสอบแหล่งเอกสารเพื่อดูสัญกรณ์ Markdown):
-
กราฟนางเงือก
-
Statediagram-V2
ทิศทาง LR
classdef s_default fill: สีขาว, สี: สีดำ
classdef s_inactive fill: สีขาว, สี: สีดำ
classdef s_parallel color: สีดำ, เติม: สีขาว
classdef s_active สี: สีแดง, เติม: darksalmon
classdef s_previous สี: สีน้ำเงิน, เติม: Azure
รัฐ "a" เป็นก
คลาส A s_previous
รัฐ "B" เป็น B
คลาส B S_ACTION
รัฐ "C" เป็น C
C -> [*]
คลาส C s_default
รัฐ C {
รัฐ "1" เป็น c_1
รัฐ C_1 {
[*] -> c_1_a
ระบุว่า "a" เป็น c_1_a
สถานะ "b" เป็น c_1_b
C_1_B -> [*]
-
-
รัฐ "2" เป็น C_2
รัฐ c_2 {
[*] -> c_2_a
ระบุว่า "A" เป็น c_2_a
สถานะ "b" เป็น c_2_b
c_2_b -> [*]
-
-
C -> A: รีเซ็ต
A -> B: init
b -> c: ทำ
c_1_a -> c_1_b: ไป
c_2_a -> c_2_b: ไป
[*] -> a
หากต้องการใช้ฟังก์ชั่นการทำกราฟที่ซับซ้อนมากขึ้นคุณจะต้องติดตั้ง graphviz
และ/หรือ pygraphviz
ในการสร้างกราฟด้วย package graphviz
คุณต้องติดตั้ง graphviz ด้วยตนเองหรือผ่านตัวจัดการแพ็คเกจ
sudo apt-get install graphviz graphviz-dev # Ubuntu and Debian
brew install graphviz # MacOS
conda install graphviz python-graphviz # (Ana)conda
ตอนนี้คุณสามารถติดตั้งแพ็คเกจ Python จริง
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
ปัจจุบัน GraphMachine
จะใช้ pygraphviz
เมื่อพร้อมใช้งานและถอยกลับไปที่ graphviz
เมื่อไม่พบ pygraphviz
หาก graphviz
ไม่พร้อมใช้งาน mermaid
จะถูกใช้ สิ่งนี้สามารถแทนที่ได้โดยผ่าน graph_engine="graphviz"
(หรือ "mermaid"
) ไปยังตัวสร้าง โปรดทราบว่าค่าเริ่มต้นนี้อาจเปลี่ยนแปลงได้ในอนาคตและการสนับสนุน pygraphviz
อาจลดลง ด้วย Model.get_graph()
คุณสามารถรับกราฟปัจจุบันหรือภูมิภาคที่น่าสนใจ (ROI) และวาดเช่นนี้:
# 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' )
สิ่งนี้สร้างสิ่งนี้:
เป็นอิสระจากแบ็กเอนด์ที่คุณใช้ฟังก์ชั่นการวาดยังยอมรับตัวบ่งชี้ไฟล์หรือสตรีมไบนารีเป็นอาร์กิวเมนต์แรก หากคุณตั้งค่าพารามิเตอร์นี้เป็น None
การส่งคืนสตรีมไบต์:
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 ()
การอ้างอิงและ partials ผ่านการโทรกลับจะได้รับการแก้ไขให้ดีที่สุดเท่าที่จะทำได้:
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' )
สิ่งนี้ควรสร้างสิ่งที่คล้ายกับสิ่งนี้:
หากรูปแบบของการอ้างอิงไม่เหมาะกับความต้องการของคุณคุณสามารถแทนที่วิธีการคงที่ GraphMachine.format_references
หากคุณต้องการข้ามการอ้างอิงทั้งหมดให้ปล่อยให้ GraphMachine.format_references
กลับมา None
นอกจากนี้ให้ดูที่สมุดบันทึก Ipython/Jupyter ตัวอย่างของเราสำหรับตัวอย่างที่มีรายละเอียดเพิ่มเติมเกี่ยวกับวิธีการใช้และแก้ไขกราฟ
ในกรณีที่การส่งเหตุการณ์ทำในเธรดหนึ่งสามารถใช้ทั้ง LockedMachine
หรือ LockedHierarchicalMachine
ที่ การเข้าถึงฟังก์ชั่น (! sic) ปลอดภัยด้วยการล็อค reentrant สิ่งนี้ไม่ได้ช่วยให้คุณเสียหายจากการทำลายเครื่องของคุณด้วยการซ่อมแซมตัวแปรสมาชิกของรุ่นหรือเครื่องสถานะของคุณ
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
ตัวจัดการบริบท Python ใด ๆ สามารถส่งผ่านผ่านอาร์กิวเมนต์คำหลักของ 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 ])
บริบทใด ๆ ผ่าน machine_model
จะถูกแชร์ระหว่างทุกรุ่นที่ลงทะเบียนกับ Machine
สามารถเพิ่มบริบทต่อโมเดลได้เช่นกัน:
lock3 = RLock ()
machine . add_model ( model , model_context = lock3 )
เป็นสิ่งสำคัญที่ผู้จัดการบริบทที่ผู้ใช้ให้ไว้ทั้งหมดจะกลับเข้ามาอีกครั้งเนื่องจากเครื่องรัฐจะเรียกพวกเขาหลายครั้งแม้ในบริบทของการเรียกใช้ทริกเกอร์เดียว
หากคุณใช้ Python 3.7 หรือใหม่กว่าคุณสามารถใช้ AsyncMachine
เพื่อทำงานกับการโทรกลับแบบอะซิงโครนัส คุณสามารถผสมการเรียกกลับแบบซิงโครนัสและอะซิงโครนัสได้หากคุณต้องการ แต่อาจมีผลข้างเคียงที่ไม่พึงประสงค์ โปรดทราบว่าเหตุการณ์จะต้องรอและต้องจัดการกับเหตุการณ์
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 ()
ดังนั้นทำไมคุณต้องใช้ Python 3.7 หรือใหม่กว่าคุณอาจถาม การสนับสนุน Async ได้รับการแนะนำก่อนหน้านี้ AsyncMachine
ใช้ประโยชน์จาก contextvars
เพื่อจัดการกับการเรียกใช้การโทรกลับเมื่อเหตุการณ์ใหม่มาถึงก่อนที่การเปลี่ยนแปลงจะเสร็จสิ้น:
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
ตัวอย่างนี้แสดงให้เห็นถึงสองสิ่ง: ประการแรกว่า 'ไป' เรียกว่าในการเปลี่ยนแปลงของ M1 จาก A
เป็น B
ไม่ได้ถูกยกเลิกและที่สองการโทร m2.fix()
จะหยุดการเปลี่ยนแปลงการเปลี่ยนแปลงของ M2 จาก A
ถึง B
โดยดำเนินการ 'Fix' จาก A
ถึง C
การแยกนี้จะเป็นไปไม่ได้หากไม่มี contextvars
โปรดทราบว่า prepare
และ conditions
จะไม่ถือว่าเป็นการเปลี่ยนผ่านอย่างต่อเนื่อง ซึ่งหมายความว่าหลังจากได้รับการประเมิน conditions
การเปลี่ยนแปลงจะถูกดำเนินการแม้ว่าเหตุการณ์อื่นจะเกิดขึ้นแล้ว งานจะถูกยกเลิกเมื่อเรียกใช้เป็น before
การโทรกลับหรือในภายหลัง
AsyncMachine
มีโหมดคิวแบบรุ่นพิเศษซึ่งสามารถใช้งานได้เมื่อ queued='model'
ถูกส่งผ่านไปยังตัวสร้าง ด้วยคิวเฉพาะรุ่นเหตุการณ์จะถูกคิวเมื่อพวกเขาอยู่ในโมเดลเดียวกัน นอกจากนี้ข้อยกเว้นที่เพิ่มขึ้นจะล้างคิวเหตุการณ์ของโมเดลที่ยกข้อยกเว้นดังกล่าวเท่านั้น เพื่อความเรียบง่ายสมมติว่าทุกเหตุการณ์ใน asyncio.gather
ด้านล่างจะไม่ถูกกระตุ้นในเวลาเดียวกัน แต่ล่าช้าเล็กน้อย:
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
โปรดทราบว่าโหมดคิวจะต้องไม่เปลี่ยนแปลงหลังการก่อสร้างเครื่อง
หากฮีโร่ของคุณต้องการพฤติกรรมที่กำหนดเองคุณสามารถโยนฟังก์ชั่นพิเศษบางอย่างโดยสถานะเครื่องตกแต่ง:
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
ปัจจุบันการเปลี่ยนแปลงมาพร้อมกับคุณสมบัติของสถานะต่อไปนี้:
หมดเวลา - ทริกเกอร์เหตุการณ์หลังจากผ่านไประยะหนึ่งแล้ว
timeout
(int, ไม่บังคับ) - ถ้าผ่านไปสถานะที่ป้อนจะหมดเวลาหลัง timeout
วินาทีon_timeout
(สตริง/เรียกได้เป็นทางเลือก) - จะถูกเรียกเมื่อถึงเวลาหมดเวลาAttributeError
เมื่อตั้ง timeout
แต่ on_timeout
ไม่ได้Tags - เพิ่มแท็กในรัฐ
tags
(รายการ, ไม่บังคับ) - กำหนดแท็กให้กับสถานะState.is_<tag_name>
True
tag_name
False
ข้อผิดพลาด - เพิ่ม MachineError
เมื่อสถานะไม่ถูกทิ้งไว้
Tags
(ถ้าคุณใช้ Error
อย่าใช้ Tags
)accepted
(บูล, ไม่บังคับ) - ทำเครื่องหมายสถานะตามที่ยอมรับtags
คำหลักที่มี 'ยอมรับ'auto_transitions
เป็น False
มิฉะนั้นทุกรัฐสามารถออกได้ด้วยวิธี to_<state>
ความผันผวน - เริ่มต้นวัตถุทุกครั้งที่มีการป้อนสถานะ
volatile
(คลาสไม่บังคับ) - ทุกครั้งที่สถานะถูกป้อนวัตถุประเภทคลาสคลาสจะถูกกำหนดให้กับโมเดล ชื่อแอตทริบิวต์ถูกกำหนดโดย hook
หากถูกละเว้นจะมีการสร้าง VolatileObject ที่ว่างเปล่าแทนhook
(string, default = 'scope') - ชื่อแอตทริบิวต์ของโมเดลสำหรับวัตถุชั่วคราว คุณสามารถเขียนส่วนขยาย State
ของคุณเองและเพิ่มวิธีเดียวกัน เพียงโปรดทราบว่า add_state_features
คาดว่าจะ ผสม ซึ่งหมายความว่าส่วนขยายของคุณควรเรียกใช้วิธีการแทนที่ __init__
, enter
และ exit
ส่วนขยายของคุณอาจสืบทอดมาจาก รัฐ แต่ก็จะทำงานได้หากไม่มี การใช้ @add_state_features
มีข้อเสียเปรียบซึ่งเป็นเครื่องที่ตกแต่ง CustomState
สามารถดองได้ นี่อาจเป็นเหตุผลในการเขียนคลาสรัฐที่กำหนดเองโดยเฉพาะแทน ขึ้นอยู่กับเครื่องรัฐที่เลือกคลาสสถานะที่กำหนดเองของคุณอาจจำเป็นต้องให้คุณสมบัติบางอย่างของรัฐ ตัวอย่างเช่น HierarchicalMachine
ต้องการสถานะที่กำหนดเองของคุณให้เป็นตัวอย่างของ NestedState
( State
ไม่เพียงพอ) ในการฉีดสถานะของคุณคุณสามารถกำหนดให้กับแอตทริบิวต์คลาสของ Machine
ของคุณ state_cls
หรือ justride 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 )
หากคุณต้องการหลีกเลี่ยงเธรดใน AsyncMachine
ของคุณทั้งหมดคุณสามารถแทนที่คุณสมบัติสถานะ Timeout
ด้วย AsyncTimeout
จากส่วนขยาย 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
คุณควรพิจารณาผ่าน queued=True
กับตัวสร้าง TimeoutMachine
สิ่งนี้จะทำให้แน่ใจว่าเหตุการณ์ได้รับการประมวลผลตามลำดับและหลีกเลี่ยงเงื่อนไขการแข่งแบบอะซิงโครนัสที่อาจปรากฏขึ้นเมื่อหมดเวลาและเหตุการณ์เกิดขึ้นในบริเวณใกล้เคียง
คุณสามารถดูคำถามที่พบบ่อยสำหรับแรงบันดาลใจหรือเช็คเอาต์ django-transitions
มันได้รับการพัฒนาโดย Christian Ledermann และยังเป็นเจ้าภาพใน GitHub เอกสารประกอบมีตัวอย่างการใช้งานบางอย่าง
ก่อนอื่นขอแสดงความยินดี! คุณมาถึงจุดสิ้นสุดของเอกสาร! หากคุณต้องการลองช่วง transitions
ก่อนที่จะติดตั้งคุณสามารถทำได้ในสมุดบันทึก Jupyter แบบโต้ตอบที่ mybinder.org เพียงคลิกปุ่มนี้
สำหรับรายงานข้อผิดพลาดและปัญหาอื่น ๆ โปรดเปิดปัญหาเกี่ยวกับ GitHub
สำหรับคำถามการใช้งานโพสต์บนสแต็กล้นตรวจสอบให้แน่ใจว่าได้ติดแท็กคำถามของคุณด้วยแท็ก pytransitions
อย่าลืมดูตัวอย่างเพิ่มเติม!
สำหรับคำถามอื่น ๆ การชักชวนหรือของขวัญทางการเงินขนาดใหญ่ที่ไม่ จำกัด อีเมล Tal Yarkoni (ผู้เขียนครั้งแรก) และ/หรือ Alexander Neumann (ผู้ดูแลปัจจุบัน)