【问题标题】:pytransitions/transitions: Is there any better way to store the history of visited state?pytransitions/transitions:有没有更好的方法来存储访问状态的历史?
【发布时间】:2021-03-01 14:47:39
【问题描述】:

我最近在 Python 中发现了一个轻量级、面向对象的状态机实现,称为转换 (https://github.com/pytransitions/transitions)。所以我尝试使用这些状态机,尤其是 HierarchicalGraphMachine。我想要的一个很好的功能是即使机器没有移动(保持相同的状态),也可以存储访问状态的历史记录。

根据我从示例中看到的情况,我们实际上无法以简单的方式做到这一点,因为当机器状态未更改时,不会调用 before_state_changeafter_state_change。所以我们不能在这种情况下扩展我们的历史。为了解决这个问题,我最终创建了一个 trigger_wrapper 函数:

def trigger_wrapper(self, trigger_name):
        
        previous_state = self.state

        result = None
        try:
            result = self.trigger(trigger_name)
        except AttributeError as attribute_err:
            print('Invalid trigger name: {}'.format(attribute_err))
        except MachineError as machine_err:
            print('Valid trigger name but not reachable: {}'.format(machine_err))
        except Exception as err:
            print('Cannot make transition with unknown error: {}'.format(err))
        
        if result is False:
            print('Trigger name reachable but condition(s) was not fulfilled')
            ....

        current_state = self.state

        # update history
        ..... 

        return result

然后,我们调用 trigger_wrapper 而不是 trigger:

before: machine.trigger('drink')
now:    machine.trigger_wrapper('drink').

除此之外,通过在初始化Machine时设置ignore_invalid_triggers = False并使用这个trigger_wrapper函数,我们现在可以通过缓存异常知道机器无法移动的原因。

有没有更好的解决方案来持续跟踪访问状态?我认为另一种方法是覆盖触发功能,但由于NestedState,它似乎很复杂。

编辑 1(按照@aleneum 的建议)

感谢您的回复以及一个有趣的例子!!!

按照使用finalize_event 的示例进行操作。很顺利,但是这个回调函数似乎不足以捕捉到以下情况(我在代码中额外添加了2行):

... same setup as before
m.go()
m.internal()
m.reflexive()
m.condition()
m.go()    # MachineError: "Can't trigger event go from state B!"
m.goo()   # AttributeError: Do not know event named 'goo'.

>>> Expected: ['go', 'internal', 'reflexive', 'condition', 'go', 'goo'] 
>>> Expected: ['B', 'B', 'B', 'B', 'B', 'B']

换句话说,是否还有另一个回调可以捕获由调用invalid trigger(示例中为goo)或由valid trigger but not reachable from the current state(从状态B 调用go())引起的异常?

再次感谢您的帮助。

【问题讨论】:

    标签: python transition pytransitions


    【解决方案1】:

    正如您已经提到的,before_state_changeafter_state_change 仅在发生转换时被调用。这并不一定意味着状态改变,因为内部和自反转换也会触发这些回调:

    from transitions import Machine
    
    
    def test():
        print("triggered")
    
    
    m = Machine(states=['A', 'B'], transitions=[
        ['go', 'A', 'B'],
        dict(trigger='internal', source='B', dest=None),
        dict(trigger='reflexive', source='B', dest='='),
        dict(trigger='condition', source='B', dest='A', conditions=lambda: False)
    ], after_state_change=test, initial='A')
    
    
    m.go()  # >>>  triggered
    m.internal()  # >>> triggered
    m.reflexive()  # >>> triggered
    m.condition()  # no output
    

    这里唯一不触发after_state_change 的事件是m.condition,因为转换被(未完成的)条件停止。

    因此,当您的目标是跟踪实际进行的转换时,after_state_change 是正确的位置。如果你想记录每个触发器/事件,你可以通过finalize_event

    'machine.finalize_event' - 即使没有发生转换或引发异常,也会执行回调

    from transitions import Machine
    
    
    event_log = []
    state_log = []
    
    
    def log_trigger(event_data):
        event_log.append(event_data.event.name)
        state_log.append(event_data.state)
    
    
    m = Machine(states=['A', 'B'], transitions=[
        ['go', 'A', 'B'],
        dict(trigger='internal', source='B', dest=None),
        dict(trigger='reflexive', source='B', dest='='),
        dict(trigger='condition', source='B', dest='A', conditions=lambda event_data: False)
    ], finalize_event=log_trigger, initial='A', send_event=True)
    
    
    m.go()
    m.internal()
    m.reflexive()
    m.condition()
    
    print(event_log)  # >>> ['go', 'internal', 'reflexive', 'condition']
    print([state.name for state in state_log])  # >>> ['B', 'B', 'B', 'B']
    

    将始终调用传递给finalize_event 的回调,即使转换引发了异常。通过设置send_event=True,所有回调将接收到一个EvenData 对象,其中包含事件、状态和转换信息以及如果出现问题的错误对象。这是我必须更改条件 lambda 表达式的方式。当send_event=True 时,所有回调都需要能够处理EventData 对象。

    关于finalize_event和回调执行顺序的更多信息可以在文档的this section中找到。

    如何也记录无效事件?

    finalize_event 仅对有效事件调用,这意味着该事件必须存在并且必须在当前源状态下有效。如果应该处理所有事件,Machine 需要扩展:

    from transitions import Machine
    
    log = []
    
    
    class LogMachine(Machine):
    
        def _get_trigger(self, model, trigger_name, *args, **kwargs):
            res = super(LogMachine, self)._get_trigger(model, trigger_name, *args, **kwargs)
            log.append((trigger_name, model.state))
            return res
    
    # ...
    m = LogMachine(states=..., ignore_invalid_triggers=True)
    assert m.trigger("go")  # valid
    assert not m.trigger("go")  # invalid
    assert not m.trigger("gooo")  # invalid
    print(log)  # >>> [('go', 'B'), ('go', 'B'), ('gooo', 'B')]
    

    每个模型都装饰有trigger 方法,该方法是Machine._get_trigger 的一部分,并分配了model 参数。 Model.trigger 可用于按名称触发事件,也可用于处理不存在的事件。当事件无效时,您还需要传递ignore_invalid_triggers=True 以不引发MachineError

    但是,如果应该记录所有事件,则从 Machine 中拆分日志记录并在处理事件的位置处理日志记录可能更可行/可维护,例如:

    m = Machine(..., ignore_invalid_triggers=True)
    # ...
    def on_event(event_name):
       logging.debug(f"Received event {event_name}")  # or log_event.append(event_name)
       m.trigger(event_name)
       logging.debug(f"Machine state is {m.state}")  # or log_state.append(m.state)
    

    【讨论】:

    • @nghia:您可以使用ignore_invalid_triggers=True 忽略无效触发器。 finalize_event 但是,只会为有效事件调用。如果您希望机器记录所有事件,则需要扩展 Machine 并使用 Model.trigger 按名称调用事件。
    • 嗨@aleneum,非常感谢。覆盖“_get_trigger”是我需要的。
    猜你喜欢
    • 1970-01-01
    • 2022-09-27
    • 1970-01-01
    • 2014-04-25
    • 2012-11-14
    • 1970-01-01
    • 2021-02-23
    • 1970-01-01
    • 2018-01-30
    相关资源
    最近更新 更多