Управление состоянием через объекты. Конечные автоматы (FSM). Библиотека transitions и state_machine.
State превращает условия в объекты. FSM превращает хаос в порядок.
State — поведенческий паттерн, позволяющий объекту изменять поведение при изменении внутреннего состояния через переходы между объектами состояний.
# ❌ ПЛОХО: Много if/elif для состояний
class Order:
def __init__(self):
self.state = "new"
def pay(self):
if self.state == "new":
self.state = "paid"
print("Order paid")
elif self.state == "paid":
print("Already paid")
elif self.state == "shipped":
print("Cannot pay shipped order")
elif self.state == "cancelled":
print("Cannot pay cancelled order")
else:
raise ValueError(f"Unknown state: {self.state}")
def ship(self):
if self.state == "paid":
self.state = "shipped"
print("Order shipped")
elif self.state == "shipped":
print("Already shipped")
elif self.state == "new":
print("Cannot ship unpaid order")
else:
print(f"Cannot ship from {self.state}")
def cancel(self):
if self.state in ("new", "paid"):
self.state = "cancelled"
print("Order cancelled")
elif self.state == "shipped":
print("Cannot ship shipped order")
else:
print(f"Cannot cancel from {self.state}")
# Проблема:
# 1. Нарушен Open-Closed — добавление состояния требует изменения всех методов
# 2. Условия размазаны по всем методам
# 3. Трудно понять допустимые переходы# ✅ ХОРОШО: Каждое состояние — класс
from abc import ABC, abstractmethod
class OrderState(ABC):
@abstractmethod
def pay(self, order: "Order"):
pass
@abstractmethod
def ship(self, order: "Order"):
pass
@abstractmethod
def cancel(self, order: "Order"):
pass
@property
@abstractmethod
def name(self) -> str:
pass
class NewState(OrderState):
@property
def name(self) -> str:
return "new"
def pay(self, order: "Order"):
order.state = PaidState()
print("Order paid")
def ship(self, order: "Order"):
print("Cannot ship unpaid order")
def cancel(self, order: "Order"):
order.state = CancelledState()
print("Order cancelled")
class PaidState(OrderState):
@property
def name(self) -> str:
return "paid"
def pay(self, order: "Order"):
print("Already paid")
def ship(self, order: "Order"):
order.state = ShippedState()
print("Order shipped")
def cancel(self, order: "Order"):
order.state = CancelledState()
print("Order cancelled, refund initiated")
class ShippedState(OrderState):
@property
def name(self) -> str:
return "shipped"
def pay(self, order: "Order"):
print("Cannot pay shipped order")
def ship(self, order: "Order"):
print("Already shipped")
def cancel(self, order: "Order"):
print("Cannot cancel shipped order")
class CancelledState(OrderState):
@property
def name(self) -> str:
return "cancelled"
def pay(self, order: "Order"):
print("Cannot pay cancelled order")
def ship(self, order: "Order"):
print("Cannot ship cancelled order")
def cancel(self, order: "Order"):
print("Already cancelled")
class Order:
def __init__(self):
self.state: OrderState = NewState()
def pay(self):
self.state.pay(self)
def ship(self):
self.state.ship(self)
def cancel(self):
self.state.cancel(self)
# Использование
order = Order()
order.pay() # Order paid
order.ship() # Order shipped
order.cancel() # Cannot cancel shipped order# ✅ FSM через таблицу переходов
from typing import Dict, Tuple, Optional
class StateMachine:
def __init__(self, initial_state: str):
self._state = initial_state
self._transitions: Dict[Tuple[str, str], str] = {}
self._handlers: Dict[str, callable] = {}
def add_transition(self, from_state: str, event: str, to_state: str,
handler: callable = None):
self._transitions[(from_state, event)] = to_state
if handler:
self._handlers[(from_state, event)] = handler
def trigger(self, event: str) -> bool:
key = (self._state, event)
if key not in self._transitions:
print(f"Invalid transition: {self._state} --{event}--> ?")
return False
new_state = self._transitions[key]
print(f"{self._state} --{event}--> {new_state}")
if key in self._handlers:
self._handlers[key]()
self._state = new_state
return True
@property
def state(self) -> str:
return self._state
# Использование
fsm = StateMachine("new")
# Добавляем переходы
fsm.add_transition("new", "pay", "paid")
fsm.add_transition("new", "cancel", "cancelled")
fsm.add_transition("paid", "ship", "shipped")
fsm.add_transition("paid", "cancel", "cancelled")
fsm.add_transition("shipped", "deliver", "delivered")
fsm.add_transition("cancelled", "restore", "new")
fsm.trigger("pay") # new --pay--> paid
fsm.trigger("ship") # paid --ship--> shipped
fsm.trigger("deliver") # shipped --deliver--> delivered# ✅ FSM с контекстом
from dataclasses import dataclass, field
from typing import Any
@dataclass
class Transition:
to_state: str
handler: callable = None
condition: callable = None
class FSM:
def __init__(self, initial_state: str):
self._state = initial_state
self._context: dict = {}
self._transitions: Dict[str, Dict[str, Transition]] = {}
def add_transition(self, from_state: str, event: str, to_state: str,
handler: callable = None, condition: callable = None):
if from_state not in self._transitions:
self._transitions[from_state] = {}
self._transitions[from_state][event] = Transition(to_state, handler, condition)
def trigger(self, event: str, **data) -> bool:
if self._state not in self._transitions:
return False
if event not in self._transitions[self._state]:
return False
transition = self._transitions[self._state][event]
# Проверка условия
if transition.condition and not transition.condition(self._context, data):
print(f"Condition failed for {event}")
return False
# Обновление контекста
self._context.update(data)
# Выполнение перехода
print(f"{self._state} --{event}--> {transition.to_state}")
if transition.handler:
transition.handler(self._context)
self._state = transition.to_state
return True
@property
def state(self) -> str:
return self._state
# Использование
fsm = FSM("idle")
def check_balance(ctx, data):
return data.get("amount", 0) <= ctx.get("balance", 0)
def deduct(ctx):
ctx["balance"] -= ctx.get("amount", 0)
print(f"Balance: ${ctx['balance']}")
fsm.add_transition("idle", "withdraw", "processing",
handler=deduct, condition=check_balance)
fsm.add_transition("processing", "complete", "idle")
fsm._context["balance"] = 100
fsm.trigger("withdraw", amount=50) # idle --withdraw--> processing, Balance: $50
fsm.trigger("complete") # processing --complete--> idle# ✅ transitions — готовая FSM библиотека
# pip install transitions
from transitions import Machine
class Order:
states = ["new", "paid", "shipped", "delivered", "cancelled"]
def __init__(self):
self.machine = Machine(
model=self,
states=Order.states,
initial="new",
)
# Добавляем переходы
self.machine.add_transition("pay", "new", "paid")
self.machine.add_transition("ship", "paid", "shipped")
self.machine.add_transition("deliver", "shipped", "delivered")
self.machine.add_transition("cancel", ["new", "paid"], "cancelled")
self.machine.add_transition("restore", "cancelled", "new")
def on_enter_paid(self):
print("Order paid, sending confirmation email")
def on_enter_shipped(self):
print("Order shipped, sending tracking number")
def on_enter_delivered(self):
print("Order delivered, marking complete")
# Использование
order = Order()
print(order.state) # new
order.pay()
print(order.state) # paid
order.ship()
print(order.state) # shipped
order.deliver()
print(order.state) # delivered# ✅ Connection State Machine
class ConnectionState:
def connect(self, conn): pass
def disconnect(self, conn): pass
def send(self, conn, data): pass
class Disconnected(ConnectionState):
def connect(self, conn):
conn.state = Connected()
print("Connected")
class Connected(ConnectionState):
def disconnect(self, conn):
conn.state = Disconnected()
print("Disconnected")
def send(self, conn, data):
print(f"Sent: {data}")
class Connection:
def __init__(self):
self.state: ConnectionState = Disconnected()
def connect(self):
self.state.connect(self)
def disconnect(self):
self.state.disconnect(self)
def send(self, data):
self.state.send(self, data)
# Использование
conn = Connection()
conn.send("Hello") # Ничего (disconnected)
conn.connect() # Connected
conn.send("Hello") # Sent: Hello
conn.disconnect() # Disconnected# ✅ Game State Machine
from enum import Enum, auto
class GameState(Enum):
MENU = auto()
PLAYING = auto()
PAUSED = auto()
GAME_OVER = auto()
class Game:
def __init__(self):
self.state = GameState.MENU
self.score = 0
def start(self):
if self.state == GameState.MENU:
self.state = GameState.PLAYING
self.score = 0
print("Game started")
else:
print("Cannot start from this state")
def pause(self):
if self.state == GameState.PLAYING:
self.state = GameState.PAUSED
print("Game paused")
else:
print("Cannot pause now")
def resume(self):
if self.state == GameState.PAUSED:
self.state = GameState.PLAYING
print("Game resumed")
else:
print("Cannot resume now")
def game_over(self):
if self.state == GameState.PLAYING:
self.state = GameState.GAME_OVER
print(f"Game Over! Score: {self.score}")
else:
print("Cannot game over now")
def restart(self):
if self.state == GameState.GAME_OVER:
self.state = GameState.MENU
print("Back to menu")
else:
print("Cannot restart now")
# Использование
game = Game()
game.start() # Game started
game.pause() # Game paused
game.resume() # Game resumed
game.game_over() # Game Over! Score: 0
game.restart() # Back to menu| Паттерн | Когда использовать | Pythonic-реализация |
|---|---|---|
| State (классы) | Сложная логика состояний с разным поведением | Классы состояний + контекст |
| FSM (таблица) | Простые переходы, конфигурируемые | dict + transitions |
| transitions | Production-ready FSM | Библиотека transitions |
Главный принцип: State инкапсулирует поведение состояния, FSM определяет допустимые переходы.
Изучите тему Strategy и Dependency Injection для семейства алгоритмов.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.