많은 확장 기능을 갖춘 Python의 경량 객체 지향 상태 머신 구현입니다. Python 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
객체 생성자에 전달하여 초기화 중에 지정하거나 나중에 추가할 수 있습니다.
편의를 위해 Machine
에 새 State
추가될 때마다 on_enter_«state name»
및 on_exit_«state name»
메서드가 Machine(모델이 아님)에 동적으로 생성되어 새로운 Enter 및 Exit를 동적으로 추가할 수 있습니다. 필요할 경우 나중에 콜백하세요.
# 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()
콜백이 정의되어 있고 initial='A'
로 Machine
초기화하는 경우 다음에 A
상태로 들어갈 때까지 on_enter_A()
실행되지 않습니다. (초기화 시 on_enter_A()
가 실행되는지 확인해야 하는 경우 간단히 더미 초기 상태를 만든 다음 __init__
메서드 내에서 to_A()
명시적으로 호출하면 됩니다.)
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
상태로 전환될 때마다 Matter
클래스에 정의된 on_enter_A()
메서드가 실행됩니다.
final=True
상태가 입력될 때 트리거되는 on_final
콜백을 사용할 수 있습니다.
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
객체를 검색하려면 Machine
인스턴스의 get_state()
메서드를 통해 수행할 수 있습니다.
lump . state
> >> 'solid'
lump . is_gas ()
> >> False
lump . is_solid ()
> >> True
machine . get_state ( lump . state ). name
> >> 'solid'
원하는 경우 Machine
을 초기화하는 동안 model_attribute
인수를 전달하여 고유한 상태 속성 이름을 선택할 수 있습니다. 하지만 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'를 입력할 수 없는 경우) 열거형을 사용하는 것이 원하는 것일 수 있습니다.
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
아직 자리를 차지하지 않은 경우 모델에 편의 메서드만 첨부하므로 모델에 이벤트 트리거와 동일한 이름의 메서드가 포함 되어서는 안 된다는 의미이기도 합니다. 해당 동작을 수정하려면 FAQ를 살펴보세요.
이제 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
이라고 하며 다음 섹션에서 소개합니다.
명시적으로 추가된 전환 외에도 상태가 Machine
인스턴스에 추가될 때마다 to_«state»()
메서드가 자동으로 생성됩니다. 이 메서드는 현재 머신의 상태에 관계없이 대상 상태로 전환됩니다.
lump . to_liquid ()
lump . state
> >> 'liquid'
lump . to_solid ()
lump . state
> >> 'solid'
원하는 경우 Machine
초기화에서 auto_transitions=False
설정하여 이 동작을 비활성화할 수 있습니다.
지정된 트리거는 여러 전환에 연결될 수 있으며, 그 중 일부는 잠재적으로 동일한 상태에서 시작되거나 끝날 수 있습니다. 예를 들어:
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()
호출하면 모델 상태가 현재 'plasma'
이면 'solid'
로 설정되고 그렇지 않으면 '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는 Machine
클래스에 add_ordered_transitions()
메서드를 제공합니다.
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' ])
위의 예에서 모델이 'solid'
상태에 있을 때 heat()
호출하면 is_flammable
True
반환하면 'gas'
상태로 전환됩니다. 그렇지 않고 is_really_hot
True
반환하면 'liquid'
상태로 전환됩니다.
편의를 위해 조건과 똑같이 동작하지만 반전된 'unless'
인수도 있습니다.
machine . add_transition ( 'heat' , 'solid' , 'gas' , unless = [ 'is_flammable' , 'is_really_hot' ])
이 경우 is_flammable()
및 is_really_hot()
이 모두 False
반환하는 경우 heat()
실행될 때마다 모델이 고체에서 기체로 전환됩니다.
조건 확인 방법은 선택적 인수 및/또는 트리거 방법에 전달된 데이터 객체를 수동적으로 수신합니다. 예를 들어 다음 호출은 다음과 같습니다.
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?"
전환이 시작되자마자 'conditions'
확인하거나 다른 콜백이 실행되기 전에 실행되는 'prepare'
콜백도 있습니다.
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
호출되지 않습니다.
모든 전환 전이나 후에 실행되도록 의도된 기본 작업은 각각 before_state_change
및 after_state_change
사용하여 초기화 중에 Machine
에 전달될 수 있습니다.
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) 다른 콜백 실행 중에 오류가 발생한 경우에도 독립적으로 실행되어야 하는 콜백에 대한 두 가지 키워드가 있습니다. prepare_event
사용하여 Machine
에 전달된 콜백은 가능한 전환(및 개별 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
살펴볼 수 있습니다.
때로는 모델의 현재 상태를 반영하는 일부 데이터를 시스템 초기화 시 등록된 콜백 함수에 전달해야 하는 경우가 있습니다. 전환을 사용하면 두 가지 방법으로 이 작업을 수행할 수 있습니다.
먼저(기본값) 모든 위치 또는 키워드 인수를 트리거 메서드( 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에서는 데이터 전송을 위한 대체 방법을 지원합니다. Machine
초기화 시 send_event=True
설정하면 트리거에 대한 모든 인수가 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는 유연하며 두 가지 다른 초기화 패턴을 지원합니다.
첫째, 다른 모델이 전혀 필요하지 않은 독립형 상태 머신을 생성할 수 있습니다. 초기화 중에 모델 인수를 생략하기만 하면 됩니다.
machine = Machine ( states = states , transitions = transitions , initial = 'solid' )
machine . melt ()
machine . state
> >> 'liquid'
이런 방식으로 머신을 초기화하면 모든 트리거 이벤트( evaporate()
, sublimate()
등)와 모든 콜백 함수를 Machine
인스턴스에 직접 연결할 수 있습니다.
이 접근 방식은 모든 상태 머신 기능을 한 곳에 통합하는 이점이 있지만, 상태 로직이 별도의 컨트롤러가 아닌 모델 자체에 포함되어야 한다고 생각한다면 약간 부자연스러울 수 있습니다.
대안(잠재적으로 더 나은) 접근 방식은 모델이 Machine
클래스에서 상속되도록 하는 것입니다. Transitions은 상속을 원활하게 지원하도록 설계되었습니다. (단, Machine
클래스의 __init__
메소드를 재정의해야 합니다!):
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(model=[Machine.self_literal, model1, ...])
와 같이 초기화 중에 클래스 변수 자리 표시자(문자열) Machine.self_literal
전달할 수 있습니다. 독립형 머신을 생성하고 생성자에 model=None
전달하여 machine.add_model
통해 모델을 동적으로 등록할 수도 있습니다. 또한 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
전환에는 매우 기본적인 로깅 기능이 포함되어 있습니다. 상태 변경, 전환 트리거, 조건부 확인 등 다양한 이벤트가 표준 Python logging
모듈을 사용하여 INFO 수준 이벤트로 기록됩니다. 이는 스크립트에서 표준 출력에 대한 로깅을 쉽게 구성할 수 있음을 의미합니다.
# 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"
모든 편의 함수를 사용하고 일부 콜백을 혼합에 포함시키려는 경우 많은 상태와 전환이 정의되어 있으면 모델 정의가 상당히 복잡해질 수 있습니다. transitions
의 generate_base_model
메소드는 기계 구성에서 기본 모델을 생성하여 이를 수행하는 데 도움을 줄 수 있습니다.
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
전환의 핵심은 경량으로 유지되지만 기능을 확장하기 위한 다양한 MixIn이 있습니다. 현재 지원되는 것은 다음과 같습니다:
원하는 기능이 활성화된 상태 머신 인스턴스를 검색하는 메커니즘에는 두 가지가 있습니다. 첫 번째 접근 방식은 기능이 필요한 경우 네 개의 매개변수 graph
, nested
, locked
또는 asyncio
True
로 설정된 편의 factory
를 사용합니다.
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
에서 직접 가져올 수도 있습니다. 명명 체계는 다음과 같습니다.
다이어그램 | 중첩됨 | 잠김 | 아시시오 | |
---|---|---|---|---|
기계 | ✘ | ✘ | ✘ | ✘ |
그래프머신 | ✓ | ✘ | ✘ | ✘ |
계층적 머신 | ✘ | ✓ | ✘ | ✘ |
잠긴기계 | ✘ | ✘ | ✓ | ✘ |
계층적 그래프머신 | ✓ | ✓ | ✘ | ✘ |
잠긴그래프기계 | ✓ | ✘ | ✓ | ✘ |
잠긴 계층적 시스템 | ✘ | ✓ | ✓ | ✘ |
잠긴 계층적 그래프머신 | ✓ | ✓ | ✓ | ✘ |
비동기머신 | ✘ | ✘ | ✘ | ✓ |
AsyncGraph머신 | ✓ | ✘ | ✘ | ✓ |
계층적 비동기 머신 | ✘ | ✓ | ✘ | ✓ |
계층적 비동기 그래프 머신 | ✓ | ✓ | ✘ | ✓ |
기능이 풍부한 상태 머신을 사용하려면 다음과 같이 작성할 수 있습니다.
from transitions . extensions import LockedHierarchicalGraphMachine as LHGMachine
machine = LHGMachine ( model , states , transitions )
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' ]
]
# ...
HierarchicalMachine
생성자의 initial
키워드는 중첩된 상태(예: initial='caffeinated_running'
)와 병렬 상태로 간주되는 상태 목록(예: initial=['A', 'B']
) 또는 현재 상태를 허용합니다. 이전에 언급한 옵션 중 하나여야 하는 또 다른 모델( initial=model.state
)입니다. 문자열을 전달할 때 transition
initial
하위 상태에 대한 대상 상태를 확인하고 이를 진입 상태로 사용합니다. 이는 하위 상태가 초기 상태를 언급하지 않을 때까지 재귀적으로 수행됩니다. 병렬 상태 또는 목록으로 전달된 상태는 '있는 그대로' 사용되며 더 이상 초기 평가가 수행되지 않습니다.
이전에 생성된 상태 개체는 NestedState
또는 파생 클래스 여야 합니다 . 간단한 Machine
인스턴스에 사용되는 표준 State
클래스에는 중첩에 필요한 기능이 부족합니다.
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
와 연결됩니다 . 현재 구분 기호는 밑줄('_')로 설정되어 있으므로 기본 시스템과 유사하게 작동합니다. 이는 foo
상태의 하위 상태 bar
이 foo_bar
에 의해 알려짐을 의미합니다. bar
의 하위 상태 baz
foo_bar_baz
등으로 참조됩니다. 하위 상태를 입력할 때 모든 상위 상태에 대해 enter
호출됩니다. 하위 상태를 종료하는 경우에도 마찬가지입니다. 셋째, 중첩된 상태는 부모의 전환 동작을 덮어쓸 수 있습니다. 전환이 현재 상태에 알려지지 않은 경우 상위 상태로 위임됩니다.
이는 표준 구성에서 HSM의 상태 이름에 밑줄이 포함되어서는 안 된다는 것을 의미합니다. transitions
의 경우 machine.add_state('state_name')
state_name
이라는 상태를 추가해야 하는지 아니면 상태 state
에 하위 상태 name
추가해야 하는지 알 수 없습니다. 그러나 어떤 경우에는 이것만으로는 충분하지 않습니다. 예를 들어 주 이름이 두 개 이상의 단어로 구성되어 있고 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()
로 호출됩니다. 하위 상태가 숫자로 시작하는 경우 전환은 Python의 속성 명명 체계를 준수하기 위해 자동 전환 FunctionWrapper
에 접두사 's'('3'이 's3'이 됨)를 추가합니다. 대화형 완성이 필요하지 않은 경우 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
children
대신 parallel
사용하면 transitions
전달된 목록의 모든 상태에 동시에 들어갑니다. 입력할 하위 상태는 항상 직접 하위 상태를 가리켜야 하는 initial
에 의해 정의됩니다. 새로운 기능은 상태 정의에 transitions
키워드를 전달하여 로컬 전환을 정의하는 것입니다. 위에 정의된 전환 ['go', 'a', 'b']
는 C_1
에서만 유효합니다. ['go', '2_z', '2_x']
에서 수행된 것처럼 하위 상태를 참조할 수 있지만 로컬로 정의된 전환에서는 상위 상태를 직접 참조할 수 없습니다. 상위 상태가 종료되면 해당 하위 상태도 종료됩니다. HierarchicalMachine
전환이 추가된 순서대로 고려되는 Machine
에서 알려진 전환 처리 순서 외에도 계층 구조도 고려합니다. 물체에 정의 된 전환은 먼저 평가 될 것입니다 (예 : C_1_a
C_2_z
전에 남겨 둡니다) 및 WildCard *
로 정의 된 전환은 (현재) 0.8.0 중첩 상태로 시작하는 루트 상태 (이 예제 A
, B
, C
)에 전환을 추가합니다. 직접 추가 할 수 있으며 부모 상태의 생성을 즉시 발행합니다.
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의 실험 : 주 또는 HSM 자체에서 on_final
콜백을 사용할 수 있습니다. a) 상태 자체가 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
키워드를 사용하지 않으면 두 인스턴스가 공유합니다. 이 변경은 기준으로 전달 된 State
인스턴스를 사용하는 Machine
와 더 일치하도록 수행되었습니다.
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
에 들어갈 것임을 알 수 있습니다. 이것이 바람직하지 않은 동작이고 기계가 부모 상태에서 오히려 멈추어야하는 경우, 사용자는 {'name': 'counting', 'children': counter, 'initial': False}
와 같은 거짓으로 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을 통해 쉽게 저장하고로드 할 수 있습니다 (FAQ 참조). 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
(default false) : 전환 가장자리에서 조건을 보여줍니다show_auto_transitions
(default false) : 그래프에 자동 전환이 표시됩니다show_state_attributes
(default false) : Show Callbacks (입력, 출구), 태그 및 타임 아웃 그래프전환은 상태 간의 모든 유효한 전환을 표시하는 기본 상태 다이어그램을 생성 할 수 있습니다. 기본 다이어그램 지원은 gitlab 또는 github 및 기타 웹 서비스의 Markdown 파일에서 Mermaid의 라이브 편집기와 함께 사용할 수있는 인어 상태 머신 정의를 생성합니다. 예를 들어,이 코드 :
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!" )
이 다이어그램을 생성합니다 (마크 다운 표기법을 보려면 문서 소스를 확인하십시오) :
---
인어 그래프
---
Statediagram-V2
방향 LR
ClassDef S_default Fill : White, Color : Black
ClassDef S_Inactive Fill : White, Color : Black
classDef s_parallel 색상 : 검은 색, 채우기 : 흰색
classDef s_active 색상 : 빨간색, 채우기 : Darksalmon
classDef s_previous 색상 : 파란색, 채우기 : Azure
"A"를 a로 상태로 표시하십시오
클래스 A s_previous
"b"를 b로 상태로 표시하십시오
클래스 B s_active
"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
C_2_A로 "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
설치되어 있어야합니다. 패키지 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
사용하여 PygraphViz를 사용하고 pygraphviz
찾을 수없는 경우 graphviz
로 돌아갑니다. 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 Notebooks를 살펴보십시오.
이벤트 디스패치가 스레드에서 수행되는 경우, 기능 액세스 (! sic)가 재진입 잠금 장치로 고정되는 LockedMachine
또는 LockedHierarchicalMachine
를 사용할 수 있습니다. 이로 인해 모델 또는 상태 머신의 멤버 변수를 땜질하여 기계를 손상시키지 않아도됩니다.
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 Context Manager는 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
로의 전환에서 호출 된 'GO'는 취소되지 않으며 두 번째로 m2.fix()
호출 A
의 전환 시도가 'Fix'를 실행하여 B
로의 전환 시도를 중단시킵니다. 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
(String/Callable, 선택 사항) - 시간 초과 시간에 도달하면 호출됩니다.timeout
설정되면 AttributeError
발생하지만 on_timeout
은 아닙니다.태그 - 상태에 태그를 추가합니다
tags
(목록, 선택 사항) - 상태에 태그를 지정합니다.State.is_<tag_name>
주가 tag_name
으로 태그를 받으면 True
반환합니다. else False
오류 - 상태를 남길 수 없을 때 MachineError
올립니다.
Tags
에서 상속 ( Error
사용하는 경우 Tags
사용하지 않음)accepted
(bool, 선택 사항) - 수락 된 상태를 표시합니다.tags
전달할 수 있습니다.auto_transitions
False
로 설정된 경우에만 오류가 발생합니다. 그렇지 않으면 모든 상태는 to_<state>
방법으로 종료 될 수 있습니다.휘발성 - 상태가 입력 될 때마다 객체를 초기화합니다.
volatile
(클래스, 선택 사항) - 상태가 입력 될 때마다 유형 클래스의 객체가 모델에 할당됩니다. 속성 이름은 hook
로 정의됩니다. 생략하면 빈 휘발성이 대신 생성됩니다hook
(string, default = 'scope') - 시간 객체의 모델의 속성 이름입니다. 자신의 State
확장을 작성하고 같은 방식으로 추가 할 수 있습니다. add_state_features
는 믹스 인을 기대합니다. 이는 확장자가 항상 재정의 메소드 __init__
, enter
및 exit
호출해야 함을 의미합니다. 연장은 상태 에서 상속 될 수 있지만 그것 없이도 작동합니다. @add_state_features
사용하면 장식 된 기계를 절인 할 수없는 단점이 있습니다 (보다 정확하게는 동적으로 생성 된 CustomState
절인 할 수 없습니다). 대신 전용 사용자 정의 상태 클래스를 작성하는 이유 일 수 있습니다. 선택한 상태 머신에 따라 사용자 정의 상태 클래스는 특정 상태 기능을 제공해야 할 수도 있습니다. 예를 들어, HierarchicalMachine
사용자 정의 상태가 NestedState
의 인스턴스가되어야합니다 ( State
충분하지 않음). 상태를 주입하려면 상태가 생성 될 때마다 완료 될 때마다 완료 할 때마다 완료 할 때마다 완료 할 때마다 다음을 수행 할 경우 Machine
의 클래스 속성 state_cls
에 할당 할 수 있습니다 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
상태 기능을 asyncio
확장에서 AsyncTimeout
으로 바꿀 수 있습니다.
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
이를 통해 이벤트가 순차적으로 처리되도록하고 시간 초과 및 이벤트가 근접하게 발생할 때 나타날 수있는 비동기 경주 조건을 피할 수 있습니다.
FAQ를 살펴보면 영감을 주거나 django-transitions
확인할 수 있습니다. Christian Ledermann에 의해 개발되었으며 Github에서도 주최됩니다. 문서에는 몇 가지 사용 예가 포함되어 있습니다.
먼저 축하합니다! 당신은 문서의 끝에 도달했습니다! 설치하기 전에 transitions
시도하려면 mybinder.org (Mybinder.org)의 대화식 jupyter 노트북에서이를 수행 할 수 있습니다. 이 버튼을 클릭하십시오.
버그 보고서 및 기타 문제는 GitHub에서 문제를여십시오.
사용 질문은 스택 오버플로에 게시하여 pytransitions
태그로 질문을 태그하십시오. 확장 된 예제를 보는 것을 잊지 마십시오!
다른 질문, 요청 또는 대규모 무제한 화폐 선물, 이메일 Tal Yarkoni (초기 저자) 및/또는 Alexander Neumann (현재 관리자).