在 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)与异常处理
你可以设置 conditions 或 unless 限制状态转移。只有当条件函数返回 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 状态下,又分为 standby 和 working 子状态。
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) 。通过配置 transitions 的 model_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. 进阶:在生命周期回调中读写数据库
如果需要在状态转换的 before 或 after 回调中执行数据库操作(例如:记录状态变更日志表 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)
🎯 最佳实践总结
- 单例状态机 :不要在每次请求时重新实例化
Machine,应保持Machine全局唯一,通过add_model/remove_model动态绑定查询出来的 DB 实体。 model_attribute:必须设置该参数,将其指向 ORM 托管的字段(如status或state)。- 事务一致性 :状态机的转移仅修改了内存中实体的属性,必须确保在同一数据库事务(
session.commit())中完成提交,以保证数据持久化的一致性。