
파이썬 상태 머신
코드를 만들어야 할 일이 있어서 상태도를 이용해서 만들기로 했다.
직관적으로 코드를 만들면 상태에 따라서 디버깅이 안되고 헤메기 일쑤여서 대부분 상태도로 해결한다.
파이썬을 이용해서 코드를 만들고 있으므로 Python을 이용한다.
https://pypi.org/project/python-statemachine/
이 글은 위의 사이트 내용을 정리하였다.
패키지 설치
pip install python-statemachine
먼저 위와 같이 패키지를 설치한다.
예제 1
from statemachine import StateMachine, State
....
>>> class TrafficLightMachine(StateMachine):
... "A traffic light machine"
... green = State("Green", initial=True)
... yellow = State("Yellow")
... red = State("Red")
...
... cycle = green.to(yellow) | yellow.to(red) | red.to(green)
...
... slowdown = green.to(yellow)
... stop = yellow.to(red)
... go = red.to(green)
...
... def before_cycle(self, event_data=None):
... message = event_data.kwargs.get("message", "")
... message = ". " + message if message else ""
... return "Running {} from {} to {}{}".format(
... event_data.event,
... event_data.transition.source.id,
... event_data.transition.target.id,
... message,
... )
...
... def on_enter_red(self):
... print("Don't move.")
...
... def on_exit_red(self):
... print("Go ahead!")
코드를 보면
... green = State("Green", initial=True)
... yellow = State("Yellow")
... red = State("Red")
상태 머신의 상태 이름이 정의된다.
3개의 상태를 가지고 있고 , 최초 상태는 Intial 로 설정된다.
인스턴스는 아래와 같이 만든다.
traffic_light = TrafficLightMachine()
상태머신은 아래와 같이 실행 한다.
traffic_light.cycle()
'Running cycle from green to yellow'
현재 상태의 확인은 아래와 같이 한다.
>>> traffic_light.current_state.id
'yellow'
또는 디버깅이나…. 상태에 대한 자세한 정보를 알고 싶으면 아래와 같이 된다.
traffic_light.current_state
State('Yellow', id='yellow', value='yellow', initial=False, final=False)
상태에 대한 코드상에서 체크는 아래와 같이 비교문으로 확인한다.
>>> traffic_light.current_state == TrafficLightMachine.yellow
True
>>> traffic_light.current_state == traffic_light.yellow
True
상태가 활성화 된지 여부를 확인 하기 위해서는 언제든지 아래 함수를 호출한다.
>>> traffic_light.green.is_active
False
>>> traffic_light.yellow.is_active
True
>>> traffic_light.red.is_active
False
모든 상태에 대한 표시는 아래와 같이 한다.
>>> [s.id for s in traffic_light.states]
['green', 'red', 'yellow']
모든 상태에 대한 이벤트는 아래와 같이 확인된다.
>>> [t.name for t in traffic_light.events]
['cycle', 'go', 'slowdown', 'stop']
Call an event by it’s name:
이벤트를 이름 단위로 수행 한다면 아래와 같다.
>>> traffic_light.cycle()
Don't move.
'Running cycle from yellow to red'
위의 메시지가 나오는 것을 보면
... def before_cycle(self, event_data=None):
... message = event_data.kwargs.get("message", "")
... message = ". " + message if message else ""
... return "Running {} from {} to {}{}".format(
... event_data.event,
... event_data.transition.source.id,
... event_data.transition.target.id,
... message,
... )
위의 코드에서 나오는 것을 알 수 있다.
before_cycle로서 해당 사이클에 들어가기 전에 실행되는 코드이다.
메시지를 빌드 하고
event는 cycle이
source / target 아이디를 프린트 한다.
메시지가 있으면 메시지를 프린트 한다.
실행 결과를 보면 don’t move가 cycle 전에 실행됨을 알 수 있다.
즉 ,
... def on_enter_red(self):
... print("Don't move.")
가 먼저 실행 되고
그 이후에 befoe_cycle이 수행되는 것이다.
궁굼한 점은 on_enter_red를 실행한다는 것을 어떻게 알 수 있는지 대한 것이다.
>>> traffic_light.send('cycle')
Go ahead!
'Running cycle from red to green'
이벤트를 실행하는 점이 포인트 이다.
예제 2
이번에는 상태도를 이용한 예제를 확인한다.
상태도는 아래와 같다.

코드는 아래와 같다.
import random
from statemachine import State
from statemachine import StateMachine
class GuessTheNumberMachine(StateMachine):
start = State("Start", initial=True)
low = State("Low")
high = State("High")
won = State("Won", final=True)
lose = State("Lose", final=True)
guess = (
lose.from_(low, high, cond="max_guesses_reached")
| won.from_(low, high, cond="guess_is_equal")
| low.from_(low, high, start, cond="guess_is_lower")
| high.from_(low, high, start, cond="guess_is_higher")
)
def __init__(self, max_attempts=5, lower=1, higher=5, seed=42):
self.max_attempts = max_attempts
self.lower = lower
self.higher = higher
self.guesses = 0
# lets play a not so random game, or our tests will be crazy
random.seed(seed)
self.number = random.randint(self.lower, self.higher)
super().__init__()
def max_guesses_reached(self):
return self.guesses >= self.max_attempts
def before_guess(self, number):
self.guesses += 1
print(f"You guess is {number}...")
def guess_is_lower(self, number):
return number < self.number
def guess_is_higher(self, number):
return number > self.number
def guess_is_equal(self, number):
return self.number == number
def on_enter_start(self):
print(f"(psss.. don't tell anyone the number is {self.number})")
print(
f"I'm thinking of a number between {self.lower} and {self.higher}. "
f"Can you guess what it is?"
)
def on_enter_low(self):
print("Too low. Try again.")
def on_enter_high(self):
print("Too high. Try again.")
def on_enter_won(self):
print(f"Congratulations, you guessed the number in {self.guesses} guesses!")
def on_enter_lose(self):
print(f"Oh, no! You've spent all your {self.guesses} attempts!")
start = State("Start", initial=True)
low = State("Low")
high = State("High")
won = State("Won", final=True)
lose = State("Lose", final=True)
우선 상태를 정리하고
....
guess = (
lose.from_(low, high, cond="max_guesses_reached")
| won.from_(low, high, cond="guess_is_equal")
| low.from_(low, high, start, cond="guess_is_lower")
| high.from_(low, high, start, cond="guess_is_higher")
)
....
전이 함수를 정의 한다.
여기서는 코드가 guess로 정의 된다.
lose.from_(low, hight, cond=”max_guess_reached”)
…
상태가 low 혹은 high에서
def max_guesses_reached(self):
return self.guesses >= self.max_attempts
위의 값으로 확인 한다. True가 되면 위와 같이 이동하게 된다.
lose로 오는 것은 low, high에서 조건이 위와 같으면 이동한다.
high.from_(low, high, start, cond="guess_is_higher")
high로 오는 것은 3가지 경우의 수로서
low, high, start에서 cond이 True이면 오게 된다.
condition은
def guess_is_higher(self, number):
return number > self.number
로서 정의 된다.
def on_enter_start(self):
print(f"(psss.. don't tell anyone the number is {self.number})")
print(
f"I'm thinking of a number between {self.lower} and {self.higher}. "
f"Can you guess what it is?"
)
start 상태에 들어갈 때에 하는 행동을 정의 한다.

정의 하면 하나의 턴으로 움직이는 것을 cycle 혹은 guess라는 이름으로 정의 한다.
그리고 그 턴을 수행할 때에 실행할 것이 있으면
... def before_cycle(self, event_data=None):
... message = event_data.kwargs.get("message", "")
... message = ". " + message if message else ""
... return "Running {} from {} to {}{}".format(
... event_data.event,
... event_data.transition.source.id,
... event_data.transition.target.id,
... message,
... )
before_[turn name]
으로 함수를 정의하면 된다.

상태에 들어갈 때에 실행 할 것은
on_enter_[State Name]. 으로
상태에서 나갈 때에는
on_exit_[State Name] 으로 출력한다.
만약 현재 상태가 어떤지 알고 싶으면
traffic_light.current_state == TrafficLightMachine.yellow
True
또는
>>> traffic_light.current_state == traffic_light.yellow
True
으로 알 수 있다.

사용 후기…
F.S.M을 클래스로 구현하면 구현은 간단하다.
하지만, 두가지 면에서 어렵다.
(1) 상태가 늘어날 수록 실행 시간이 늘어난다.
그냥 늘어나는 것이 아니라 느낌이 지수함수적으로 실행 시간이 늘어나는 느낌이다.
(2) 디버깅이 어렵다.
해당 상태로 이동하는 것을 정의 하는 것이 아니라
해당 상태를 오는 것을 정의 한다.
즉 현재 상태에서 다음 상태로 가는 것을 정의 해야 하는데
다음 상태에서 그 상태로 오는 것을 정의 하기 때문에 조건을 맞추기가 쉽지 않다.
한번 코드가 꼬이면 어렵게 된다.
답글 남기기