在 Python 开发中 transitions 的使用

在 Python 开发中,transitions 是最流行、功能最强大的有限状态机(Finite State Machine, FSM)库。以下为您提供一份核心优先、结构清晰的详细使用教程。


📦 安装

在 macOS 下,推荐使用 uv 管理依赖:

bash 复制代码
# 基础安装
uv pip install transitions

# 如果需要支持绘制状态转移图 (需要系统已安装 graphviz)
uv pip install transitions[diagrams]

1. 基础概念与极简示例

transitions 的核心由 States(状态)Transitions(转换关系)Machine(状态机) 组成。

python 复制代码
from transitions import Machine

# 1. 定义状态列表
states = ['off', 'on']

# 2. 定义状态转换规则
# trigger: 触发动作的方法名
# source: 转换前的状态
# dest: 转换后的目标状态
transitions = [
    {'trigger': 'turn_on', 'source': 'off', 'dest': 'on'},
    {'trigger': 'turn_off', 'source': 'on', 'dest': 'off'}
]

# 3. 初始化状态机
# 默认情况下,如果不传 model,Machine 实例本身将作为 Model
machine = Machine(states=states, transitions=transitions, initial='off')

# 4. 使用状态机
print(machine.state)  # 输出: off
machine.turn_on()     # 触发 'turn_on'
print(machine.state)  # 输出: on

2. 绑定自定义业务模型 (Model)

在实际开发中,我们通常将状态机逻辑绑定到自定义的对象上。状态机会动态地将状态控制方法注入到该对象中。

python 复制代码
from transitions import Machine

# 业务类
class LightBulb:
    def __init__(self):
        self.brightness = 100

bulb = LightBulb()
states = ['off', 'on', 'broken']

# 将状态机绑定到 bulb 实例
machine = Machine(model=bulb, states=states, initial='off')

# 动态添加转换规则
# '*' 代表可以从任何状态转换到 'broken'
machine.add_transition(trigger='burn_out', source='*', dest='broken')
machine.add_transition(trigger='turn_on', source='off', dest='on')

# 此时 bulb 对象拥有了 state 属性和触发器方法
print(bulb.state)  # 输出: off
bulb.turn_on()
print(bulb.state)  # 输出: on
bulb.burn_out()
print(bulb.state)  # 输出: broken

3. 条件限制 (Conditions)与异常处理

你可以设置 conditionsunless 限制状态转移。只有当条件函数返回 True(或 unless 返回 False)时,转换才会发生。

python 复制代码
from transitions import Machine, MachineError

class Water:
    def __init__(self):
        self.temperature = 20

    def is_hot(self):
        return self.temperature >= 100

water = Water()
states = ['liquid', 'gas']
# ignore_invalid_triggers=True 可以防止在无效状态下调用触发器抛出异常,而是返回 False
machine = Machine(model=water, states=states, initial='liquid', ignore_invalid_triggers=True)

machine.add_transition(
    trigger='evaporate', 
    source='liquid', 
    dest='gas', 
    conditions='is_hot'  # 绑定条件检查方法
)

# 尝试在 20 度时蒸发
success = water.evaporate()
print(success)      # 输出: False (转换失败)
print(water.state)  # 输出: liquid

# 加热后再次尝试
water.temperature = 100
success = water.evaporate()
print(success)      # 输出: True (转换成功)
print(water.state)  # 输出: gas

4. 生命周期回调函数 (Callbacks)

状态转换前后可以触发各种生命周期钩子:

  • prepare: 转换开始前触发(即使条件不满足也会触发)。
  • before: 确定可以开始转换时(条件满足后)触发。
  • after: 转换成功后触发。
python 复制代码
class Hero:
    def wear_armor(self):
        print("[Callback] 穿上战甲!")

    def log_success(self):
        print("[Callback] 状态成功转换。")

hero = Hero()
states = ['normal', 'combat']
machine = Machine(model=hero, states=states, initial='normal')

machine.add_transition(
    trigger='encounter_enemy', 
    source='normal', 
    dest='combat',
    before='wear_armor',
    after='log_success'
)

hero.encounter_enemy()
# 控制台输出:
# [Callback] 穿上战甲!
# [Callback] 状态成功转换。

5. 分层/嵌套状态机 (Hierarchical State Machine)

当业务复杂、状态具有层级关系时,可以使用 HierarchicalMachine。例如:设备在 on 状态下,又分为 standbyworking 子状态。

python 复制代码
from transitions.extensions import HierarchicalMachine as Machine

# 定义嵌套状态结构
states = [
    'off',
    {
        'name': 'on',
        'children': ['standby', 'working'],
        'initial': 'standby'
    }
]

machine = Machine(states=states, initial='off')

machine.add_transition('power_on', 'off', 'on')
machine.add_transition('activate', 'on_standby', 'on_working')
# 从 on 下的任何子状态均可 power_off 转换到 off
machine.add_transition('power_off', 'on', 'off')

print(machine.state)  # 输出: off
machine.power_on()
print(machine.state)  # 输出: on_standby (进入 on 状态的默认初始子状态)
machine.activate()
print(machine.state)  # 输出: on_working

6. 异步状态机 (AsyncMachine)

在 FastAPI 等异步上下文中,你可以使用 AsyncMachine。其所有回调和触发器都会以协程形式运行。

python 复制代码
import asyncio
from transitions.extensions.asyncio import AsyncMachine

class AsyncTask:
    async def notify_server(self):
        print("开始向服务器同步状态...")
        await asyncio.sleep(0.5)
        print("同步完成。")

task = AsyncTask()
states = ['pending', 'completed']
machine = AsyncMachine(model=task, states=states, initial='pending')

machine.add_transition(
    trigger='complete', 
    source='pending', 
    dest='completed',
    before='notify_server'
)

async def main():
    # 异步触发器必须使用 await
    await task.complete()
    print(f"当前状态: {task.state}")

asyncio.run(main())

在结合 PostgreSQL 使用时,最优雅的实践是使用 SQLAlchemy 2.0 (ORM) 。通过配置 transitionsmodel_attribute 参数,状态机可以直接读写 SQLAlchemy 托管的数据库字段,状态变更会被 SQLAlchemy 自动追踪,最后通过 session.commit() 持久化到 PostgreSQL。

以下是基于 异步 SQLAlchemy 2.0 + PostgreSQL 的完整持久化实践教程。


📦 1. 安装依赖

bash 复制代码
uv pip install sqlalchemy asyncpg transitions

🔑 2. 设计核心方案

我们将定义一个 Order(订单)模型,其状态字段为 status。我们将状态机设置为单例,并在从数据库查询出订单后,动态将其绑定到状态机中。

代码实现:database.py & models.py

python 复制代码
import asyncio
from sqlalchemy import String, Integer
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, DeclarativeBase
from sqlalchemy.orm import Mapped, mapped_column
from transitions import Machine

# 1. 初始化数据库连接 (请替换为您的 PostgreSQL 连接串)
DATABASE_URL = "postgresql+asyncpg://postgres:password@localhost:5432/mydb"
engine = create_async_engine(DATABASE_URL, echo=True)
async_session = async_sessionmaker(engine, expire_on_commit=False)

class Base(DeclarativeBase):
    pass

# 2. 定义 SQLAlchemy 订单模型
class Order(Base):
    __tablename__ = "orders"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    title: Mapped[str] = mapped_column(String(100))
    # 状态字段,对应状态机中的状态
    status: Mapped[str] = mapped_column(String(50), default="created")

# 3. 初始化全局状态机 (不绑定具体 model)
# model_attribute='status' 关键参数:指示状态机读写 model 的 status 属性而不是默认的 state
states = ["created", "paid", "shipped", "completed", "cancelled"]
order_machine = Machine(
    model=None,  # 初始不绑定任何 model
    states=states, 
    initial="created", 
    model_attribute="status",
    ignore_invalid_triggers=True
)

# 4. 定义转换规则
order_machine.add_transition(trigger="pay", source="created", dest="paid")
order_machine.add_transition(trigger="ship", source="paid", dest="shipped")
order_machine.add_transition(trigger="complete", source="shipped", dest="completed")
order_machine.add_transition(trigger="cancel", source=["created", "paid"], dest="cancelled")

🚀 3. 业务层使用:查询、转换与持久化

在业务逻辑中,我们从 PostgreSQL 读取 Order 实例,将其加入状态机,触发转换后提交事务。

python 复制代码
# service.py
from database import async_session, Order, order_machine
from sqlalchemy import select

async def create_new_order(title: str) -> int:
    """创建新订单,初始状态为 'created'"""
    async with async_session() as session:
        async with session.begin():
            new_order = Order(title=title)
            session.add(new_order)
        # commit 在 begin() 块结束时自动发生
        return new_order.id

async def process_order_payment(order_id: int):
    """处理订单支付:状态从 'created' -> 'paid'"""
    async with async_session() as session:
        async with session.begin():
            # 1. 从 PostgreSQL 查询订单
            result = await session.execute(select(Order).where(Order.id == order_id))
            order = result.scalar_one_or_none()
            
            if not order:
                print("订单不存在")
                return

            # 2. 将此订单实例注册到全局状态机中
            # transitions 内部使用弱引用(weakref)管理 model,不会造成内存泄漏
            order_machine.add_model(order)

            # 3. 触发转换 (会自动修改 order.status 属性)
            print(f"当前数据库状态: {order.status}")  # created
            success = order.pay()  # 触发 'pay'
            
            if success:
                print(f"内存状态已变更为: {order.status}")  # paid
                # 4. 离开 begin() 块时,SQLAlchemy 会自动发出 UPDATE 语句持久化到 PostgreSQL
            else:
                print("状态转换失败,不满足转换条件")

            # 5. 转换结束,从状态机中注销 model (可选,弱引用会自动回收,但手动移除更干净)
            order_machine.remove_model(order)

💡 4. 进阶:在生命周期回调中读写数据库

如果需要在状态转换的 beforeafter 回调中执行数据库操作(例如:记录状态变更日志表 OrderLog),可以结合异步状态机 AsyncMachine

python 复制代码
from transitions.extensions.asyncio import AsyncMachine
from sqlalchemy.ext.asyncio import AsyncSession

# 1. 声明包含 DB 回调逻辑的 Model 基类或 Mixin
class OrderWithCallback(Order):
    # 此方法将作为 transitions 的 before/after 回调运行
    async def log_status_change(self, event_data):
        # 从触发参数中获取当前的 db session
        session: AsyncSession = event_data.kwargs.get("session")
        if session:
            print(f"正在向日志表写入:订单 {self.id} 状态从 {event_data.transition.source} 变更为 {event_data.transition.dest}")
            # 执行额外的 DB 插入操作,例如: session.add(OrderLog(...))

# 2. 使用 AsyncMachine
async_order_machine = AsyncMachine(
    model=None, 
    states=states, 
    initial="created", 
    model_attribute="status"
)

async_order_machine.add_transition(
    trigger="pay", 
    source="created", 
    dest="paid",
    after="log_status_change"  # 绑定回调
)

# 3. 业务调用
async def pay_with_callback(order_id: int):
    async with async_session() as session:
        async with session.begin():
            result = await session.execute(select(OrderWithCallback).where(OrderWithCallback.id == order_id))
            order = result.scalar_one()
            
            async_order_machine.add_model(order)
            # 传入 session 供回调函数使用
            await order.pay(session=session) 

🎯 最佳实践总结

  1. 单例状态机 :不要在每次请求时重新实例化 Machine,应保持 Machine 全局唯一,通过 add_model / remove_model 动态绑定查询出来的 DB 实体。
  2. model_attribute :必须设置该参数,将其指向 ORM 托管的字段(如 statusstate)。
  3. 事务一致性 :状态机的转移仅修改了内存中实体的属性,必须确保在同一数据库事务(session.commit())中完成提交,以保证数据持久化的一致性。
相关推荐
Rust研习社1 小时前
通过手写一个迷你 grep 来学习 Rust 的所有权与借用
后端
用户531397318171 小时前
「踩坑实录」原来的SQL索引自动优化失败了,线上数据库差点被打挂
java·后端
go不是csgo2 小时前
从0到1理解Go熔断器:sony/gobreaker 源码剖析 + 仿TikTok Feed 项目实战
开发语言·后端·golang
SimonKing2 小时前
线程池面试被问到怕?看完这篇让他当场沉默
java·后端·程序员
大刚测试开发实战2 小时前
TestHub重磅更新!AI用例生成增加流式输出、Markdown文档上传、模型配置检测、AI评审开关控制...
vue.js·后端·github
JAVA9652 小时前
JAVA面试-并发篇 07-CAS底层原理是什么有什么缺陷如何解决
java·开发语言·面试
程序员阿卢2 小时前
01-基于springboot框架调用ollama下的模型完成基本功能
spring boot·后端·ollama·通义千问模型qwen
IT_陈寒2 小时前
Python列表的+=操作符坑了我一整天
前端·人工智能·后端
阿里嘎多学长2 小时前
2026-06-09 GitHub 热点项目精选
开发语言·程序员·github·代码托管