Pytransitions 排队的分层机器总是以初始状态结束

Pytransitions queued hierarchical machine ends always in init state

我将 pytransitions 与 HierarchicalMachine class 结合使用,以便能够创建小型嵌套机器以在更大的状态机中完成子任务。我正在使用排队的转换来从状态回调内部调用触发器。

我希望以下代码以 prepare_waiting 状态结束,但它实际上返回到 prepare_init 状态。

你知道为什么会这样吗?

代码:

from transitions.extensions.factory import HierarchicalMachine
import logging as log

QUEUED = True

class PrepareMachine(HierarchicalMachine):
    def __init__(self):

        states = [
            {"name": "init", "on_enter": self.entry_init},
            {"name": "connecting", "on_enter": self.entry_connecting},
            {"name": "waiting", "on_enter": self.entry_waiting},
        ]

        super().__init__(states=states, initial="init", queued=QUEUED)

    def entry_init(self):
        print("common entry point ...")
        self.to_connecting()

    def entry_connecting(self):
        print("connecting multiple indtruments ...")
        self.to_waiting()

    def entry_waiting(self):
        print("wait for response ...")

class ProductionMachine(HierarchicalMachine):
    def __init__(self):
        prepare = PrepareMachine()
        states = ["init", {"name": "prepare", "children": prepare}]
        super().__init__(states=states, initial="init", queued=QUEUED)
        self.add_transition("start_testing", "init", "prepare")

log.basicConfig(level=log.INFO)
machine = ProductionMachine()
machine.start_testing()
print(machine.state)

输出:

INFO:transitions.core:Finished processing state init exit callbacks.
INFO:transitions.core:Finished processing state prepare enter callbacks.
common entry point ...
INFO:transitions.core:Finished processing state init exit callbacks.
connecting multiple indtruments ...
INFO:transitions.core:Executed callback '<bound method PrepareMachine.entry_connecting of <__main__.PrepareMachine object at 0xb6588bd0>>'
INFO:transitions.core:Finished processing state connecting enter callbacks.
INFO:transitions.core:Finished processing state connecting exit callbacks.
wait for response ...
INFO:transitions.core:Executed callback '<bound method PrepareMachine.entry_waiting of <__main__.PrepareMachine object at 0xb6588bd0>>'
INFO:transitions.core:Finished processing state waiting enter callbacks.
INFO:transitions.core:Executed callback '<bound method PrepareMachine.entry_init of <__main__.PrepareMachine object at 0xb6588bd0>>'
INFO:transitions.core:Finished processing state init enter callbacks.
prepare_init

不足:PrepareMachine 的回调中的 self 没有引用正确的模型。

长:

发生了什么,为什么?

要理解为什么会发生这种情况,必须考虑 pytransitions 的概念,它将状态机拆分为包含所有状态、事件和转换的 'rule book'(例如 Machine)定义和通常称为模型的有状态对象。所有便利功能,如触发功能(以转换名称命名的方法)或自动转换(如 to_<state>)都附加到模型。

machine = Machine(model=model, states=states, transitions=transitions, initial="initial")
assert model.is_initial()  # 
model.to_work()  # auto transition
assert model.is_work()
machine.to_initial() <-- will raise an exception

当您不将模型参数传递给 Machine 时,机器本身将充当模型,从而获得附加到它的所有便利和触发功能。

machine = Machine(states=states, transitions=transitions, initial="A")
assert machine.is_A()
machine.to_B()
assert machine.is_B()

因此,在您的示例中,prepare = PrepareMachine() 使 prepare 充当其自己的模型,而 machine = ProductionMachine() 使 machine 成为 ProductionMachine 的模型。这就是为什么您可以调用 prepare.to_connecting() 的原因,因为 prepapre 也是一个模型。但是,不是您想要的模型 machine。所以如果我们稍微改变一下你的例子,事情可能会变得更清楚一点:

class ProductionMachine(HierarchicalMachine):
    def __init__(self):
        self.prepare = PrepareMachine()
        states = ["init", {"name": "prepare", "children": self.prepare}]
# [...]
machine = ProductionMachine()
machine.start_testing()
print(machine.state)  #  >>> prepare_init
print(machine.prepare.state)  # >>> waiting

使用 machine.start_testing(),您让 machine 输入 prepare_init,然后调用 PrepareMachine.entry_init。在此方法中,您调用 self.to_connecting(),它会触发从 prepareconnectingNOT machine 的转换。当 prepare 进入 connecting 时,将调用 PrepareMachine.entry_connecting 并且 self(又名 prepare)将再次转换为 to_waiting。由于 PrepareMachineProductionMachine 处理排队的事件,prepare 将完成 to_connecting 并立即处理 to_waiting。此时 machine 仍在处理 entry_init,因为 self.to_connection(又名 prepare.to_connecting)尚未 ​​return。因此,当 prepare 最终达到 waiting 状态时,machine 将 return 并记录它现在已完成 start_testing 的处理转换回调。模型 machine 没有恢复到 prepare_initprepare 的所有处理发生 WHILE start_testing 被处理,这导致日志start_testing 的消息包装所有其他消息。

如何实现你想要的?

我们想在正确的模型 (machine) 上触发事件 (to_connecting/waiting)。有多种方法可以解决这个问题。首先,我建议定义 'proper' 转换而不是依赖自动转换。自动转换也会传递给 machine(因此 machine.to_connecting 会起作用),当您有多个同名子状态时,事情可能会变得混乱。

选项 A:从 event_data 获取正确的模型。

当您将 send_event=True 传递给 Machine 构造函数时,每个回调都可以(并且必须)接受一个 EventData 对象,该对象包含有关当前处理的转换的所有信息。这包括模型。

        transitions = [
            ["connect", "init", "connecting"],
            ["connected", "connecting", "waiting"]
        ]

        states = [
            {"name": "init", "on_enter": self.entry_init},
            {"name": "connecting", "on_enter": self.entry_connecting},
            {"name": "waiting", "on_enter": self.entry_waiting},
        ]
# ...

    def entry_init(self, event_data):
        print("common entry point ...")
        event_data.model.connect()
        # we could use event_data.model.to_connecting() as well
        # but I'd recommend defining transitions with 'proper' names
        # focused on events

    def entry_connecting(self, event_data):
        print("connecting multiple instruments ...")
        event_data.model.connected()

    def entry_waiting(self, event_data):
        print("wait for response ...")
# ...

        super().__init__(states=states, transitions=transitions, initial="init", queued=QUEUED, send_event=True)

选项 B:使用回调名称而不是引用并将它们直接传递给 on_enter

当回调参数是名称时,transitions 将在当前处理的模型上解析回调。参数 on_enter 允许传递多个回调,还可以混合引用和字符串。所以你的代码看起来像这样。

from transitions.extensions.factory import HierarchicalMachine
import logging as log

QUEUED = False


class PrepareMachine(HierarchicalMachine):
    def __init__(self):
     
        transitions = [
            ["connect", "init", "connecting"],
            ["connected", "connecting", "waiting"]
        ]

        states = [
            {"name": "init", "on_enter": [self.entry_init, "connect"]},
            {"name": "connecting", "on_enter": [self.entry_connecting, "connected"]},
            {"name": "waiting", "on_enter": self.entry_waiting},
        ]
        super().__init__(states=states, transitions=transitions, initial="init", queued=QUEUED)

    def entry_init(self):
        print("common entry point ...")

    def entry_connecting(self):
        print("connecting multiple indtruments ...")

    def entry_waiting(self):
        print("wait for response ...")


class ProductionMachine(HierarchicalMachine):
    def __init__(self):
        self.prepare = PrepareMachine()
        states = ["init", {"name": "prepare", "children": self.prepare}]
        super().__init__(states=states, initial="init", queued=QUEUED)
        self.add_transition("start_testing", "init", "prepare")

log.basicConfig(level=log.INFO)
machine = ProductionMachine()
machine.start_testing()
assert machine.is_prepare_waiting()

请注意,我不得不切换 QUEUED=False,因为在 transitions 0.8.8 和更早版本中,有一个 bug 与嵌套转换的排队处理相关。 更新: 这个错误已经在刚刚发布的 transitions 0.8.9 中修复。 QUEUED=True 现在应该也可以工作了。