Python開發(fā)中transitions的使用小結(jié)
在 Python 開發(fā)中,transitions 是最流行、功能最強大的有限狀態(tài)機(Finite State Machine, FSM)庫。以下為您提供一份核心優(yōu)先、結(jié)構(gòu)清晰的詳細(xì)使用教程。
?? 安裝
在 macOS 下,推薦使用 uv 管理依賴:
# 基礎(chǔ)安裝 uv pip install transitions # 如果需要支持繪制狀態(tài)轉(zhuǎn)移圖 (需要系統(tǒng)已安裝 graphviz) uv pip install transitions[diagrams]
1. 基礎(chǔ)概念與極簡示例
transitions 的核心由 States(狀態(tài))、Transitions(轉(zhuǎn)換關(guān)系) 和 Machine(狀態(tài)機) 組成。
from transitions import Machine
# 1. 定義狀態(tài)列表
states = ['off', 'on']
# 2. 定義狀態(tài)轉(zhuǎn)換規(guī)則
# trigger: 觸發(fā)動作的方法名
# source: 轉(zhuǎn)換前的狀態(tài)
# dest: 轉(zhuǎn)換后的目標(biāo)狀態(tài)
transitions = [
{'trigger': 'turn_on', 'source': 'off', 'dest': 'on'},
{'trigger': 'turn_off', 'source': 'on', 'dest': 'off'}
]
# 3. 初始化狀態(tài)機
# 默認(rèn)情況下,如果不傳 model,Machine 實例本身將作為 Model
machine = Machine(states=states, transitions=transitions, initial='off')
# 4. 使用狀態(tài)機
print(machine.state) # 輸出: off
machine.turn_on() # 觸發(fā) 'turn_on'
print(machine.state) # 輸出: on
2. 綁定自定義業(yè)務(wù)模型 (Model)
在實際開發(fā)中,我們通常將狀態(tài)機邏輯綁定到自定義的對象上。狀態(tài)機會動態(tài)地將狀態(tài)控制方法注入到該對象中。
from transitions import Machine
# 業(yè)務(wù)類
class LightBulb:
def __init__(self):
self.brightness = 100
bulb = LightBulb()
states = ['off', 'on', 'broken']
# 將狀態(tài)機綁定到 bulb 實例
machine = Machine(model=bulb, states=states, initial='off')
# 動態(tài)添加轉(zhuǎn)換規(guī)則
# '*' 代表可以從任何狀態(tài)轉(zhuǎn)換到 'broken'
machine.add_transition(trigger='burn_out', source='*', dest='broken')
machine.add_transition(trigger='turn_on', source='off', dest='on')
# 此時 bulb 對象擁有了 state 屬性和觸發(fā)器方法
print(bulb.state) # 輸出: off
bulb.turn_on()
print(bulb.state) # 輸出: on
bulb.burn_out()
print(bulb.state) # 輸出: broken
3. 條件限制 (Conditions)與異常處理
你可以設(shè)置 conditions 或 unless 限制狀態(tài)轉(zhuǎn)移。只有當(dāng)條件函數(shù)返回 True(或 unless 返回 False)時,轉(zhuǎn)換才會發(fā)生。
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 可以防止在無效狀態(tài)下調(diào)用觸發(fā)器拋出異常,而是返回 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 度時蒸發(fā)
success = water.evaporate()
print(success) # 輸出: False (轉(zhuǎn)換失敗)
print(water.state) # 輸出: liquid
# 加熱后再次嘗試
water.temperature = 100
success = water.evaporate()
print(success) # 輸出: True (轉(zhuǎn)換成功)
print(water.state) # 輸出: gas
4. 生命周期回調(diào)函數(shù) (Callbacks)
狀態(tài)轉(zhuǎn)換前后可以觸發(fā)各種生命周期鉤子:
prepare: 轉(zhuǎn)換開始前觸發(fā)(即使條件不滿足也會觸發(fā))。before: 確定可以開始轉(zhuǎn)換時(條件滿足后)觸發(fā)。after: 轉(zhuǎn)換成功后觸發(fā)。
class Hero:
def wear_armor(self):
print("[Callback] 穿上戰(zhàn)甲!")
def log_success(self):
print("[Callback] 狀態(tài)成功轉(zhuǎn)換。")
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] 穿上戰(zhàn)甲!
# [Callback] 狀態(tài)成功轉(zhuǎn)換。
5. 分層/嵌套狀態(tài)機 (Hierarchical State Machine)
當(dāng)業(yè)務(wù)復(fù)雜、狀態(tài)具有層級關(guān)系時,可以使用 HierarchicalMachine。例如:設(shè)備在 on 狀態(tài)下,又分為 standby 和 working 子狀態(tài)。
from transitions.extensions import HierarchicalMachine as Machine
# 定義嵌套狀態(tài)結(jié)構(gòu)
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 下的任何子狀態(tài)均可 power_off 轉(zhuǎn)換到 off
machine.add_transition('power_off', 'on', 'off')
print(machine.state) # 輸出: off
machine.power_on()
print(machine.state) # 輸出: on_standby (進(jìn)入 on 狀態(tài)的默認(rèn)初始子狀態(tài))
machine.activate()
print(machine.state) # 輸出: on_working
6. 異步狀態(tài)機 (AsyncMachine)
在 FastAPI 等異步上下文中,你可以使用 AsyncMachine。其所有回調(diào)和觸發(fā)器都會以協(xié)程形式運行。
import asyncio
from transitions.extensions.asyncio import AsyncMachine
class AsyncTask:
async def notify_server(self):
print("開始向服務(wù)器同步狀態(tài)...")
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():
# 異步觸發(fā)器必須使用 await
await task.complete()
print(f"當(dāng)前狀態(tài): {task.state}")
asyncio.run(main())
在結(jié)合 PostgreSQL 使用時,最優(yōu)雅的實踐是使用 SQLAlchemy 2.0 (ORM)。通過配置 transitions 的 model_attribute 參數(shù),狀態(tài)機可以直接讀寫 SQLAlchemy 托管的數(shù)據(jù)庫字段,狀態(tài)變更會被 SQLAlchemy 自動追蹤,最后通過 session.commit() 持久化到 PostgreSQL。
以下是基于 異步 SQLAlchemy 2.0 + PostgreSQL 的完整持久化實踐教程。
?? 1. 安裝依賴
uv pip install sqlalchemy asyncpg transitions
?? 2. 設(shè)計核心方案
我們將定義一個 Order(訂單)模型,其狀態(tài)字段為 status。我們將狀態(tài)機設(shè)置為單例,并在從數(shù)據(jù)庫查詢出訂單后,動態(tài)將其綁定到狀態(tài)機中。
代碼實現(xiàn):database.py&models.py
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. 初始化數(shù)據(jù)庫連接 (請?zhí)鎿Q為您的 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))
# 狀態(tài)字段,對應(yīng)狀態(tài)機中的狀態(tài)
status: Mapped[str] = mapped_column(String(50), default="created")
# 3. 初始化全局狀態(tài)機 (不綁定具體 model)
# model_attribute='status' 關(guān)鍵參數(shù):指示狀態(tài)機讀寫 model 的 status 屬性而不是默認(rèn)的 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. 定義轉(zhuǎn)換規(guī)則
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. 業(yè)務(wù)層使用:查詢、轉(zhuǎn)換與持久化
在業(yè)務(wù)邏輯中,我們從 PostgreSQL 讀取 Order 實例,將其加入狀態(tài)機,觸發(fā)轉(zhuǎn)換后提交事務(wù)。
# service.py
from database import async_session, Order, order_machine
from sqlalchemy import select
async def create_new_order(title: str) -> int:
"""創(chuàng)建新訂單,初始狀態(tài)為 'created'"""
async with async_session() as session:
async with session.begin():
new_order = Order(title=title)
session.add(new_order)
# commit 在 begin() 塊結(jié)束時自動發(fā)生
return new_order.id
async def process_order_payment(order_id: int):
"""處理訂單支付:狀態(tài)從 '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. 將此訂單實例注冊到全局狀態(tài)機中
# transitions 內(nèi)部使用弱引用(weakref)管理 model,不會造成內(nèi)存泄漏
order_machine.add_model(order)
# 3. 觸發(fā)轉(zhuǎn)換 (會自動修改 order.status 屬性)
print(f"當(dāng)前數(shù)據(jù)庫狀態(tài): {order.status}") # created
success = order.pay() # 觸發(fā) 'pay'
if success:
print(f"內(nèi)存狀態(tài)已變更為: {order.status}") # paid
# 4. 離開 begin() 塊時,SQLAlchemy 會自動發(fā)出 UPDATE 語句持久化到 PostgreSQL
else:
print("狀態(tài)轉(zhuǎn)換失敗,不滿足轉(zhuǎn)換條件")
# 5. 轉(zhuǎn)換結(jié)束,從狀態(tài)機中注銷 model (可選,弱引用會自動回收,但手動移除更干凈)
order_machine.remove_model(order)
?? 4. 進(jìn)階:在生命周期回調(diào)中讀寫數(shù)據(jù)庫
如果需要在狀態(tài)轉(zhuǎn)換的 before 或 after 回調(diào)中執(zhí)行數(shù)據(jù)庫操作(例如:記錄狀態(tài)變更日志表 OrderLog),可以結(jié)合異步狀態(tài)機 AsyncMachine。
from transitions.extensions.asyncio import AsyncMachine
from sqlalchemy.ext.asyncio import AsyncSession
# 1. 聲明包含 DB 回調(diào)邏輯的 Model 基類或 Mixin
class OrderWithCallback(Order):
# 此方法將作為 transitions 的 before/after 回調(diào)運行
async def log_status_change(self, event_data):
# 從觸發(fā)參數(shù)中獲取當(dāng)前的 db session
session: AsyncSession = event_data.kwargs.get("session")
if session:
print(f"正在向日志表寫入:訂單 {self.id} 狀態(tài)從 {event_data.transition.source} 變更為 {event_data.transition.dest}")
# 執(zhí)行額外的 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" # 綁定回調(diào)
)
# 3. 業(yè)務(wù)調(diào)用
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 供回調(diào)函數(shù)使用
await order.pay(session=session)
?? 最佳實踐總結(jié)
- 單例狀態(tài)機:不要在每次請求時重新實例化 Machine,應(yīng)保持 Machine 全局唯一,通過 add_model / remove_model 動態(tài)綁定查詢出來的 DB 實體。
- model_attribute:必須設(shè)置該參數(shù),將其指向 ORM 托管的字段(如 status 或 state)。
- 事務(wù)一致性:狀態(tài)機的轉(zhuǎn)移僅修改了內(nèi)存中實體的屬性,必須確保在同一數(shù)據(jù)庫事務(wù)(session.commit())中完成提交,以保證數(shù)據(jù)持久化的一致性。
到此這篇關(guān)于Python開發(fā)中transitions的使用小結(jié)的文章就介紹到這了,更多相關(guān)Python transitions使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python利用 utf-8-sig 編碼格式解決寫入 csv 文件亂碼問題
這篇文章主要介紹了Python利用 utf-8-sig 編碼格式解決寫入 csv 文件亂碼問題,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2020-02-02
sublime3之內(nèi)網(wǎng)安裝python插件Anaconda的流程
這篇文章主要介紹了sublime3之內(nèi)網(wǎng)安裝python插件Anaconda的流程,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11
vscode和Anaconda安裝以及相關(guān)環(huán)境配置指南
Anaconda是一個開源的python發(fā)行版本,是現(xiàn)在比較流行的python數(shù)據(jù)科學(xué)平臺,可以對python的科學(xué)包做到有效管理,這篇文章主要給大家介紹了關(guān)于vscode和Anaconda安裝以及相關(guān)環(huán)境配置指南的相關(guān)資料,需要的朋友可以參考下2023-11-11
pyspark自定義UDAF函數(shù)調(diào)用報錯問題解決
這篇文章主要為大家介紹了pyspark自定義UDAF函數(shù)調(diào)用報錯問題解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06

