تطبيق آلة حالة خفيف الوزن وموجه نحو الكائنات في بايثون مع العديد من الامتدادات. متوافق مع بايثون 2.7+ و3.0+.
pip install transitions
... أو انسخ الريبو من GitHub ثم:
python setup.py install
يقولون أن المثال الجيد يساوي 100 صفحة من وثائق API، أو مليون توجيه، أو ألف كلمة.
حسنًا، "إنهم" ربما يكذبون... ولكن هنا مثال على أي حال:
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
.
قم بإلقاء نظرة على ملحقات المخططات إذا كنت تريد معرفة الطريقة.
آلة الحالة هي نموذج للسلوك يتكون من عدد محدود من الحالات والانتقالات بين تلك الحالات. داخل كل حالة وانتقال يمكن تنفيذ بعض الإجراءات . تحتاج آلة الحالة إلى البدء عند حالة أولية معينة. عند استخدام transitions
، قد تتكون آلة الحالة من كائنات متعددة حيث تحتوي بعض ( الآلات ) على تعريفات لمعالجة ( نماذج ) أخرى. أدناه، سننظر في بعض المفاهيم الأساسية وكيفية العمل معها.
ولاية . تمثل الحالة حالة أو مرحلة معينة في آلة الدولة. إنها طريقة مميزة للسلوك أو مرحلة في العملية.
انتقال . هذه هي العملية أو الحدث الذي يتسبب في تغيير آلة الحالة من حالة إلى أخرى.
نموذج . هيكل الدولة الفعلي. إنه الكيان الذي يتم تحديثه أثناء التحولات. وقد يحدد أيضًا الإجراءات التي سيتم تنفيذها أثناء التحولات. على سبيل المثال، مباشرة قبل الانتقال أو عند دخول الحالة أو الخروج منها.
آلة . هذا هو الكيان الذي يدير ويتحكم في النموذج والحالات والانتقالات والإجراءات. إنه الموصل الذي ينظم العملية الكاملة لآلة الدولة.
مشغل . هذا هو الحدث الذي يبدأ عملية انتقال، وهي الطريقة التي ترسل الإشارة لبدء عملية انتقال.
فعل . عملية أو مهمة محددة يتم تنفيذها عند الدخول إلى حالة معينة أو الخروج منها أو أثناء فترة انتقالية. يتم تنفيذ الإجراء من خلال عمليات الاسترجاعات ، وهي وظائف يتم تنفيذها عند وقوع حدث ما.
يعد تشغيل جهاز الحالة أمرًا بسيطًا جدًا. لنفترض أن لديك 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»
ديناميكيًا على الجهاز (وليس على النموذج!)، مما يسمح لك بإضافة إدخال وخروج جديد ديناميكيًا عمليات الاسترجاعات لاحقًا إذا كنت في حاجة إليها.
# 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
والتي سيتم تشغيلها عند إدخال الحالة مع 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
الفعلي للحالة الحالية، فيمكنك القيام بذلك من خلال طريقة 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
يمكنك مزج التعدادات والسلاسل إذا أردت (على سبيل المثال [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
(في هذه الحالة، 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
enter
لن تتم معالجة عمليات الاسترجاعات المتعلقة بالحالة أو exit
. لتحديد انتقال ليكون داخليًا، قم بتعيين الوجهة على 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?"
هناك أيضًا كلمتان رئيسيتان لعمليات رد الاتصال التي يجب تنفيذها بشكل مستقل أ) لعدد التحولات الممكنة، ب) في حالة نجاح أي انتقال، و ج) حتى إذا ظهر خطأ أثناء تنفيذ بعض عمليات رد الاتصال الأخرى. سيتم تنفيذ عمليات الاسترجاعات التي تم تمريرها إلى 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
كما ربما تكون قد أدركت بالفعل، فإن الطريقة القياسية لتمرير العناصر القابلة للاستدعاء إلى الحالات والشروط والانتقالات هي بالاسم. عند معالجة عمليات الاسترجاعات والشروط، ستستخدم transitions
اسمها لاسترداد العناصر القابلة للاستدعاء ذات الصلة من النموذج. إذا تعذر استرداد الطريقة وكانت تحتوي على نقاط، فستتعامل transitions
مع الاسم كمسار إلى وظيفة الوحدة النمطية وتحاول استيراده. وبدلاً من ذلك، يمكنك تمرير أسماء الخصائص أو السمات. سيتم تضمينها في وظائف ولكن لا يمكنها تلقي بيانات الحدث لأسباب واضحة. يمكنك أيضًا تمرير العناصر القابلة للاستدعاء مثل الوظائف (المنضمة) مباشرةً. كما ذكرنا سابقًا، يمكنك أيضًا تمرير قوائم/مجموعات من أسماء المواد القابلة للاستدعاء إلى معلمات رد الاتصال. سيتم تنفيذ عمليات الاسترجاعات بالترتيب الذي تمت إضافتها به.
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
للترابط.
في بعض الأحيان تحتاج إلى تمرير بعض البيانات التي تعكس الحالة الحالية للنموذج إلى وظائف رد الاتصال المسجلة عند تهيئة الجهاز. تتيح لك Transitions القيام بذلك بطريقتين مختلفتين.
أولاً (الافتراضي)، يمكنك تمرير أي وسيطات موضعية أو رئيسية مباشرةً إلى توابع التشغيل (التي يتم إنشاؤها عند استدعاء 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()
vs.machine.add_transition machine.add_transition()
).
ولحسن الحظ، فإن Transitions مرنة، وتدعم نمطين آخرين من أنماط التهيئة.
أولاً، يمكنك إنشاء جهاز حالة مستقل لا يتطلب طرازًا آخر على الإطلاق. ما عليك سوى حذف وسيطة النموذج أثناء التهيئة:
machine = Machine ( states = states , transitions = transitions , initial = 'solid' )
machine . melt ()
machine . state
> >> 'liquid'
إذا قمت بتهيئة الجهاز بهذه الطريقة، فيمكنك بعد ذلك إرفاق جميع أحداث التشغيل (مثل evaporate()
و sublimate()
وما إلى ذلك) وجميع وظائف رد الاتصال مباشرة بمثيل Machine
.
يتمتع هذا الأسلوب بميزة دمج جميع وظائف آلة الحالة في مكان واحد، ولكنه قد يبدو غير طبيعي إلى حد ما إذا كنت تعتقد أنه يجب تضمين منطق الحالة داخل النموذج نفسه بدلاً من وحدة تحكم منفصلة.
هناك نهج بديل (من المحتمل أن يكون أفضل) وهو أن يرث النموذج من فئة Machine
. تم تصميم التحولات لدعم الميراث بسلاسة. (فقط تأكد من تجاوز طريقة __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'
يمكنك هنا دمج جميع وظائف جهاز الحالة في نموذجك الحالي، والذي غالبًا ما يبدو أكثر طبيعية من لصق جميع الوظائف التي نريدها في مثيل 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
. نظام التسمية هو كما يلي:
المخططات | متداخلة | مغلق | غير متزامن | |
---|---|---|---|---|
آلة | ✘ | ✘ | ✘ | ✘ |
GraphMachine | ✓ | ✘ | ✘ | ✘ |
HierarchicalMachine | ✘ | ✓ | ✘ | ✘ |
LockedMachine | ✘ | ✘ | ✓ | ✘ |
HierarchicalGraphMachine | ✓ | ✓ | ✘ | ✘ |
LockedGraphMachine | ✓ | ✘ | ✓ | ✘ |
LockedHierarchicalMachine | ✘ | ✓ | ✓ | ✘ |
Locked HierarchicalGraphMachine | ✓ | ✓ | ✓ | ✘ |
AsyncMachine | ✘ | ✘ | ✘ | ✓ |
AsyncGraphMachine | ✓ | ✘ | ✘ | ✓ |
HierarchicalAsyncMachine | ✘ | ✓ | ✘ | ✓ |
HierarchicalAsyncGraphMachine | ✓ | ✓ | ✘ | ✓ |
لاستخدام آلة حالة غنية بالميزات، يمكن للمرء أن يكتب:
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
لمنشئ 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
الحالة الفرعية من الحالة foo
سيتم معرفته بواسطة foo_bar
. ستتم الإشارة إلى الحالة الفرعية baz
bar
باسم foo_bar_baz
وما إلى ذلك. عند الدخول إلى حالة فرعية، سيتم استدعاء enter
لجميع الحالات الأصلية. وينطبق الشيء نفسه على الخروج من الحالات الفرعية. ثالثًا، يمكن للحالات المتداخلة أن تحل محل السلوك الانتقالي لآبائها. إذا لم يكن الانتقال معروفًا للحالة الحالية، فسيتم تفويضه إلى الأصل.
وهذا يعني أنه في التكوين القياسي، يجب ألا تحتوي أسماء الحالات في وحدات HSM على شرطات سفلية. بالنسبة transitions
، من المستحيل معرفة ما إذا كان يجب على machine.add_state('state_name')
إضافة حالة باسم state_name
أو إضافة name
حالة فرعية إلى state
. ولكن في بعض الحالات هذا لا يكفي. على سبيل المثال، إذا كانت أسماء الولايات تتكون من أكثر من كلمة واحدة وتريد/تحتاج إلى استخدام شرطة سفلية للفصل بينها بدلاً من CamelCase
. للتعامل مع هذا، يمكنك تغيير الحرف المستخدم للفصل بسهولة تامة. يمكنك أيضًا استخدام أحرف Unicode مميزة إذا كنت تستخدم Python 3. يؤدي تعيين الفاصل إلى شيء آخر غير الشرطة السفلية إلى تغيير بعض السلوك (الانتقال التلقائي وتعيين عمليات الاسترجاعات) بالرغم من ذلك:
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
في الاعتبار التسلسل الهرمي أيضًا. سيتم تقييم التحولات المحددة في البدائل أولاً (على سبيل المثال C_1_a
يتم تركها قبل C_2_z
) والتحولات المحددة باستخدام Wildcard *
(في الوقت الحالي) ستضيف فقط التحولات إلى الحالات الجذرية (في هذا المثال 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
وتم إدخالها للتو أو ب) تعتبر جميع البدائل نهائية ودخلت بديلاً على الأقل للتو حالة نهائية. في حالة ب) سيتم اعتبار جميع الآباء نهائيًا أيضًا إذا كان الشرط ب) ينطبق عليهم. قد يكون هذا مفيدًا في الحالات التي تحدث فيها المعالجة بالتوازي ويجب إخطار 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
مثيل الجهاز نفسه ولكن الحالات والانتقال عن طريق إنشاء نسخ منها. ومع ذلك ، نظرًا لأن حالات (Nested)State
0.8.0 (متداخلة) تتم الرجوع إليها للتو مما يعني أن التغييرات في مجموعة الحالات والأحداث لآلة واحدة ستؤثر على مثيل الجهاز الآخر. لن تتم مشاركة النماذج وحالتها. لاحظ أن الأحداث والتحولات يتم نسخها أيضًا بالرجوع إليها وسيتم مشاركتها من قبل كلتا الحالتين إذا لم تستخدم الكلمة الرئيسية 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']
لتحقيق نفس السلوك.
في الحالات التي تريد أن يتم فيها نسخ الدول والتحولات من خلال القيمة بدلاً من المرجع (على سبيل المثال ، إذا كنت ترغب في الاحتفاظ بالسلوك المسبق) ، فيمكنك القيام بذلك عن طريق إنشاء 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
بتعريف البدائل إما مع 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 Live ، في ملفات 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):
---
الرسم البياني حورية البحر
---
StertiAgram-V2
الاتجاه LR
ClassDef S_Default Fill: White ، Color: Black
classdef s_inactive تعبئة: أبيض ، اللون: أسود
ClassDef S_Paralal Color: Black ، Fill: White
ClassDef S_Active Color: Red ، Fill: Darksalmon
ClassDef S_Previous Color: Blue ، Fill: Azure
الدولة "أ" كـ
الفئة A S_Previous
الدولة "ب" كما ب
الفئة ب s_active
الدولة "ج" كما ج
C -> [*]
الفئة C S_Default
الدولة ج {
الحالة "1" مثل C_1
الحالة C_1 {
[*] -> C_1_A
الحالة "A" AS C_1_A
الحالة "B" كـ C_1_B
C_1_B -> [*]
}
--
الحالة "2" مثل C_2
الحالة C_2 {
[*] -> C_2_A
الحالة "A" AS C_2_A
الحالة "B" باسم C_2_B
C_2_B -> [*]
}
}
ج -> أ: إعادة تعيين
-> ب: init
ب -> ج: افعل
C_1_A -> C_1_B: اذهب
C_2_A -> C_2_B: اذهب
[*] -> أ
لاستخدام وظائف الرسوم البيانية الأكثر تطوراً ، ستحتاج إلى تثبيت 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 ()
سيتم حل المراجع والجزارات التي تم تمريرها مع عدادات عمليات الاسترجاعات بشكل جيد قدر الإمكان:
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) بأقفال إعادة إدخال. هذا لا يوفر لك من إفساد جهازك عن طريق العبث بمتغيرات الأعضاء في النموذج أو جهاز الحالة.
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
يوضح هذا المثال فعليًا شيئين: أولاً ، لا يتم إلغاء "Go" الذي يطلق عليه "Go" في انتقال 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
(قائمة ، اختيارية) - يعين العلامات إلى حالةState.is_<tag_name>
True
عندما يتم وضع علامة على الحالة مع tag_name
، غير False
خطأ - يرفع MachineError
عندما لا يمكن ترك الدولة
Tags
(إذا كنت تستخدم Error
لا تستخدم Tags
)accepted
(منطقي ، اختياري) - يمثل الدولة كما مقبولةtags
الكلمات الرئيسية ، والتي تحتوي على "مقبولة"auto_transitions
على False
. وإلا يمكن الخروج من كل حالة مع طرق to_<state>
.متقلبة - تهيئة كائن في كل مرة يتم فيها إدخال دولة
volatile
(فئة ، اختيارية) - في كل مرة يتم فيها إدخال الحالة كائن من فئة النوع سيتم تعيينه إلى النموذج. يتم تعريف اسم السمة بواسطة hook
. إذا تم حذفه ، سيتم إنشاء مخادع فارغ بدلاً من ذلكhook
(سلسلة ، DEFAULT = 'SCOPE') - اسم سمة النموذج للكائن الزمني. يمكنك كتابة ملحقات State
وإضافتها بنفس الطريقة. فقط لاحظ أن add_state_features
تتوقع mixins . هذا يعني أن امتدادك يجب أن يطلق دائمًا على الأساليب المتجاوز __init__
، enter
exit
. قد يرث امتدادك من الدولة ولكنه سيعمل أيضًا بدونها. باستخدام @add_state_features
له عيب وهو أن الآلات المزينة لا يمكنخلها المخلل (بدقة ، لا يمكن مخلل CustomState
الذي تم إنشاؤه ديناميكيًا). قد يكون هذا سببًا لكتابة فئة حالة مخصصة مخصصة بدلاً من ذلك. اعتمادًا على آلة الحالة المختارة ، قد تحتاج فئة الحالة المخصصة إلى توفير ميزات معينة للدولة. على سبيل المثال ، تتطلب HierarchicalMachine
أن تكون حالتك المخصصة مثالًا لـ NestedState
( State
ليست كافية). لحقن حالاتك ، يمكنك إما تعيينها إلى state_cls
فئة Machine
الخاص بك أو 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
. تم تطويره بواسطة كريستيان ليدرمان ويتم استضافته أيضًا على جيثب. الوثائق تحتوي على بعض أمثلة الاستخدام.
أولا ، مبروك! وصلت إلى نهاية الوثائق! إذا كنت ترغب في تجربة transitions
قبل تثبيتها ، فيمكنك القيام بذلك في دفتر Jupyter التفاعلي على mybinder.org. فقط انقر فوق هذا الزر.
لتقارير الأخطاء وغيرها من القضايا ، يرجى فتح مشكلة على جيثب.
بالنسبة لأسئلة الاستخدام ، قم بنشر على STACK Overflow ، مع التأكد من وضع علامة على سؤالك مع علامة pytransitions
. لا تنسى إلقاء نظرة على الأمثلة الموسعة!
بالنسبة لأي أسئلة أخرى أو طلبات أخرى أو هدايا نقدية كبيرة غير مقيدة ، أرسل بريدًا إلكترونيًا إلى Tal Yarkoni (المؤلف الأولي) و/أو ألكساندر نيومان (المشرف الحالي).