pytransitions / transitions

A lightweight, object-oriented finite state machine implementation in Python with many extensions
MIT License
5.49k stars 524 forks source link

Example regarding loading from a pickle #518

Closed matanox closed 3 years ago

matanox commented 3 years ago

Firstly, thanks for this great library!

If I'm correct then I figure the hard way that machine.add_model() needs to be called in the following usage pattern where the model is the class encapsulating the machine, and the machine is read back from a pickle:

import pickle
import os.path
from transitions import Machine

class StateObject(object):

    def __init__(self, states, transitions):

        """ loads from pickle file if previously pickled to file """

        self.persistence_path = type(self).__name__

        if os.path.isfile(self.persistence_path):
            with open(self.persistence_path, 'rb') as f:
                self.machine = pickle.load(f)
                assert type(self.machine) == Machine
                self.machine.add_model(self)  # <=======
                print(f'state loaded from {self.persistence_path}')
        else:
            print(f'no prior state known. initializing state to {states[0]}.')
            self.machine = Machine(
                model=self,
                states=states,
                initial=states[0],
                transitions=transitions)

        print(f'current state is {self.state}')

    def persist(self):

        # persist current state to file
        with open(self.persistence_path, 'wb') as f:
            print(f'persisting state to {self.persistence_path} ...')
            pickle.dump(self.machine, f)
            print(f'state persisted to {self.persistence_path}')

    def finalize(self):
        self.persist()

states = ['fresh', 'bootstrap']
transitions = [{'trigger':'bootstrap',  'source': 'fresh', 'dest': 'bootstrap'}]
stm = StateObject(states, transitions)

# kick off the state machine
if stm.state == 'fresh': stm.trigger('bootstrap')
stm.finalize()

It seems that add_model() is necessary after the unpickling, in order that the initialization magic that adds methods to the model class happens.

If that's the case, maybe it helps to have this here or in the documentation.

matanox commented 3 years ago

In the code above, the use case is a class that takes care of persistence and initializing from a persisted machine/model.

Obviously I'm still learning which relationship works most cleanly out of those mentioned in the initialization patterns section of the docs. Admittedly not yet comfortable with the model v.s. machine dichotomy of concepts, or the rationalization for a machine relating to an object carrying a semantic notion of a "model" for the machine. It's clear to me why a container is needed for hosting functions and attributes, but help will be much appreciated on understanding why the model metaphor is involved in this way.

aleneum commented 3 years ago

Hello @matanster,

transitions considers the state machine to be a 'rule book' which is applied to the stateful model(s). If this is not required, you can omit the model parameter and let the machine itself act as model. For UC where only one model is required this is often sufficient. If you, however, have to manage multiple models, this separation can become quite handy. You can change the rulebook without touching (and even knowing) each individual model.

Considering your pickle question:

transitions.Machine and all derivatives contain a list of references to their models. So, when you pickle it, you also pickle all the models:

from transitions import Machine
import pickle

class MyClass:

    def __init__(self):
        self.machine = Machine(model=[self], states=['A', 'B'])

model = MyClass()

dmp = pickle.dumps(model.machine)

loaded = pickle.loads(dmp)
loaded_model = loaded.models[0]  # every machine has a `models` attribute with a list of models

assert not loaded_model == model
assert type(loaded_model) == type(model)

The pickled model will of course contain all the decorated methods already. If you want to add a new model as done in your example above (StateObject is more or less adding a second instance when the file exists), you do it via Machine.add_model.

This being said I'd ask you to post future questions about how to use transitions on Stack Overflow.

Your question gains higher visibility since most developers look for help there. The targeted community is larger; Some people will even help you to formulate a good question. People get 'rewarded' with 'reputation' to help you. You also gain reputation in case this questions pops up more frequently. It's a win-win situation. . Tag your question with [python] and [transitions] to make sure, that users of transitions will receive a notification. If the SO community cannot answer you question and I haven't answered in a week, you can notify me by opening a new issue.