杰瑞科技汇

Python statemachine 如何实现与使用?

Of course! Let's dive into State Machines in Python.

Python statemachine 如何实现与使用?-图1
(图片来源网络,侵删)

A State Machine (or Finite-State Machine, FSM) is a computational model used to design algorithms and programs that must manage complex logic based on their current "state" and the "events" or "inputs" they receive.

Core Concepts

Before we code, let's understand the key terms:

  1. State: A condition or situation in the life cycle of the object. For example, a VendingMachine can be in an IDLE, WAITING_FOR_COIN, or DISPENSING state.
  2. Event/Trigger: An action or occurrence that can cause the state machine to change from one state to another. For example, inserting a coin (coin_inserted event) or pressing a selection button (button_pressed event).
  3. Transition: The movement from one state to another, triggered by an event. For example, from IDLE to WAITING_FOR_COIN on the coin_inserted event.
  4. Action: An activity that is performed in response to a transition. For example, when transitioning from WAITING_FOR_COIN to DISPENSING, the action might be "dispense the product" and "return change".

Method 1: The "Pure Python" Approach (Using a Dictionary)

This is the most fundamental way to implement a state machine in Python. It's great for understanding the core logic and is perfect for simple to moderately complex machines.

We'll represent the state machine as a dictionary where:

Python statemachine 如何实现与使用?-图2
(图片来源网络,侵删)
  • The keys are the state names.
  • The values are another dictionary that maps events to the next state and an optional action.

Example: A Simple Vending Machine

Let's model a vending machine that sells one product for $1.00.

class VendingMachine:
    def __init__(self):
        # Define all possible states
        self.IDLE = 'IDLE'
        self.WAITING_FOR_COIN = 'WAITING_FOR_COIN'
        self.DISpensING = 'DISPENSING' # Corrected typo: DISPENSING
        # The current state of the machine
        self.state = self.IDLE
        # The cost of the product
        self.product_cost = 1.00
        self.current_balance = 0.00
    def _transition(self, event, *args, **kwargs):
        """
        Handles the transition logic based on the current state and event.
        This is the core of the state machine.
        """
        # Define the state transition table
        # Format: {state: {event: (next_state, action_function)}}
        transition_table = {
            self.IDLE: {
                'coin_inserted': (self.WAITING_FOR_COIN, self._add_balance)
            },
            self.WAITING_FOR_COIN: {
                'coin_inserted': (self.WAITING_FOR_COIN, self._add_balance),
                'button_pressed': (self.DISPENSING, self._dispense_product)
            },
            self.DISPENSING: {
                'product_dispensed': (self.IDLE, self._reset_balance)
            }
        }
        # Get the valid transitions for the current state
        possible_transitions = transition_table.get(self.state, {})
        # Check if the event is valid for the current state
        if event in possible_transitions:
            next_state, action_func = possible_transitions[event]
            # Execute the action associated with the transition
            if action_func:
                action_func(*args, **kwargs)
            # Update the current state
            self.state = next_state
            print(f"Transitioned to new state: {self.state}")
        else:
            print(f"Error: Event '{event}' is not valid in state '{self.state}'")
    # --- Action Methods ---
    def _add_balance(self, amount):
        """Action for adding money to the balance."""
        self.current_balance += amount
        print(f"Added ${amount:.2f}. Current balance: ${self.current_balance:.2f}")
    def _dispense_product(self):
        """Action for dispensing the product."""
        if self.current_balance >= self.product_cost:
            print("Dispensing product...")
            self.current_balance -= self.product_cost
        else:
            print("Error: Not enough balance.")
            # Stay in the current state, but we can also define a specific error state
            return
    def _reset_balance(self):
        """Action for resetting the balance after a transaction."""
        self.current_balance = 0.00
        print("Balance reset. Transaction complete.")
    # --- Public Interface (Events) ---
    def insert_coin(self, amount):
        """Public method to trigger the 'coin_inserted' event."""
        print(f"\nEvent: insert_coin(${amount:.2f}) in state '{self.state}'")
        self._transition('coin_inserted', amount)
    def press_button(self):
        """Public method to trigger the 'button_pressed' event."""
        print(f"\nEvent: press_button() in state '{self.state}'")
        self._transition('button_pressed')
    def product_taken(self):
        """Public method to trigger the 'product_dispensed' event."""
        print(f"\nEvent: product_taken() in state '{self.state}'")
        self._transition('product_dispensed')
# --- Let's run the simulation ---
if __name__ == "__main__":
    vm = VendingMachine()
    # Scenario 1: Successful purchase
    vm.insert_coin(0.50) # State: IDLE -> WAITING_FOR_COIN
    vm.insert_coin(0.50) # State: WAITING_FOR_COIN -> WAITING_FOR_COIN
    vm.press_button()     # State: WAITING_FOR_COIN -> DISPENSING
    vm.product_taken()   # State: DISPENSING -> IDLE
    print("\n" + "="*30 + "\n")
    # Scenario 2: Not enough money
    vm.insert_coin(0.25) # State: IDLE -> WAITING_FOR_COIN
    vm.press_button()     # State: WAITING_FOR_COIN -> DISPENSING (fails, but state might stay)
                         # Let's refine: the action should prevent the transition.
                         # Our current logic has a flaw. Let's fix it.
                         # A better way is for the action to return True/False to allow the transition.
                         # For simplicity, we'll assume pressing the button again after dispensing fails.
    # vm.press_button()     # This would fail as we are in DISPENSING and 'button_pressed' is not a valid event.
    # Let's re-run a corrected scenario
    print("\n" + "="*30 + "\n")
    vm = VendingMachine() # Reset
    vm.insert_coin(1.50) # State: IDLE -> WAITING_FOR_COIN
    vm.press_button()     # State: WAITING_FOR_COIN -> DISPENSING
    print(f"Final balance: ${vm.current_balance:.2f}") # Should be 0.50
    vm.product_taken()   # State: DISPENSING -> IDLE
    print(f"Final balance: ${vm.current_balance:.2f}") # Should be 0.00

Pros of this approach:

  • No external libraries needed.
  • The logic is centralized in the transition_table, making it easy to see all possible state changes.
  • Very explicit and easy to debug.

Cons:

  • Can become verbose and hard to manage for very large state machines.
  • The logic is not encapsulated within the states themselves.

Method 2: Using a Library (transitions)

For more complex applications, using a dedicated library is highly recommended. The transitions library is a powerful and popular choice. It provides a clean, object-oriented way to define states, transitions, and callbacks.

Python statemachine 如何实现与使用?-图3
(图片来源网络,侵删)

First, install it:

pip install transitions

Example: The Same Vending Machine with transitions

from transitions import Machine
class VendingMachine:
    def __init__(self):
        self.product_cost = 1.00
        self.current_balance = 0.00
        # Define states
        states = ['idle', 'waiting_for_coin', 'dispensing']
        # Define transitions
        transitions = [
            {'trigger': 'insert_coin', 'source': 'idle', 'dest': 'waiting_for_coin', 'after': 'add_balance'},
            {'trigger': 'insert_coin', 'source': 'waiting_for_coin', 'dest': 'waiting_for_coin', 'after': 'add_balance'},
            {'trigger': 'press_button', 'source': 'waiting_for_coin', 'dest': 'dispensing', 'conditions': ['has_sufficient_funds'], 'after': 'dispense_product'},
            {'trigger': 'product_taken', 'source': 'dispensing', 'dest': 'idle', 'after': 'reset_balance'},
        ]
        # Initialize the state machine
        self.machine = Machine(model=self, states=states, initial='idle', transitions=transitions)
    # --- Callbacks (Actions & Conditions) ---
    def add_balance(self, amount):
        """Callback for the 'insert_coin' transition."""
        # We need to get the amount from the event. The library passes it as a keyword argument.
        self.current_balance += amount
        print(f"Added ${amount:.2f}. Current balance: ${self.current_balance:.2f}")
    def has_sufficient_funds(self):
        """Condition for the 'press_button' transition."""
        return self.current_balance >= self.product_cost
    def dispense_product(self):
        """Callback for the 'press_button' transition."""
        print("Dispensing product...")
        self.current_balance -= self.product_cost
        print(f"Product dispensed. Remaining balance: ${self.current_balance:.2f}")
    def reset_balance(self):
        """Callback for the 'product_taken' transition."""
        print("Balance reset. Transaction complete.")
        self.current_balance = 0.00
    # --- Public Interface (Events) ---
    # The library automatically creates these methods from the 'trigger' names
    def insert_coin(self, amount):
        """Public method to insert a coin."""
        print(f"\nEvent: insert_coin(${amount:.2f}) in state '{self.state}'")
        self.insert_coin(amount) # Calls the machine's trigger
    def press_button(self):
        """Public method to press the button."""
        print(f"\nEvent: press_button() in state '{self.state}'")
        self.press_button() # Calls the machine's trigger
    def product_taken(self):
        """Public method to take the product."""
        print(f"\nEvent: product_taken() in state '{self.state}'")
        self.product_taken() # Calls the machine's trigger
# --- Let's run the simulation ---
if __name__ == "__main__":
    vm = VendingMachine()
    # Scenario 1: Successful purchase
    vm.insert_coin(0.50)
    vm.insert_coin(0.50)
    vm.press_button()
    vm.product_taken()
    print("\n" + "="*30 + "\n")
    # Scenario 2: Not enough money
    vm.insert_coin(0.25)
    vm.press_button() # This transition will NOT happen because the condition 'has_sufficient_funds' is False.
    print(f"Current state is still: {vm.state}") # Will still be 'waiting_for_coin'
    vm.insert_coin(0.75)
    vm.press_button() # Now the condition is met
    vm.product_taken()

Pros of using transitions:

  • Cleaner Code: States and transitions are defined declaratively.
  • Powerful Features: Supports nested states, state callbacks (on_enter, on_exit), conditions, and visual graphing.
  • Less Boilerplate: The library handles the dispatching logic for you.
  • Scalability: Much easier to manage for large and complex state machines.

Cons:

  • Adds an external dependency to your project.

Which Approach Should You Choose?

  • Use the Pure Python approach if:

    • Your state machine is very simple.
    • You want to avoid external dependencies.
    • You are learning the fundamentals of state machines.
  • Use the transitions library if:

    • Your state machine has more than a few states and transitions.
    • You need features like conditions or callbacks.
    • You want to visualize your state machine (vm.machine.get_graph().draw('my_state_machine.png', prog='dot')).
    • You want to write cleaner, more maintainable code. This is the recommended approach for most real-world applications.
分享:
扫描分享到社交APP
上一篇
下一篇