跳转到主要内容

一个轻量级的面向对象Python状态机实现,具有许多扩展。

项目描述

快速入门

他们说 一个好的例子胜过 100页的API文档、一百万条指令或一千个单词。

好吧,“他们”可能是在撒谎...但无论如何,这里有一个例子

from transitions import Machine
import random

class NarcolepticSuperhero(object):

    # Define some states. Most of the time, narcoleptic superheroes are just like
    # everyone else. Except for...
    states = ['asleep', 'hanging out', 'hungry', 'sweaty', 'saving the world']

    def __init__(self, name):

        # No anonymous superheroes on my watch! Every narcoleptic superhero gets
        # a name. Any name at all. SleepyMan. SlumberGirl. You get the idea.
        self.name = name

        # What have we accomplished today?
        self.kittens_rescued = 0

        # Initialize the state machine
        self.machine = Machine(model=self, states=NarcolepticSuperhero.states, initial='asleep')

        # Add some transitions. We could also define these using a static list of
        # dictionaries, as we did with states above, and then pass the list to
        # the Machine initializer as the transitions= argument.

        # At some point, every superhero must rise and shine.
        self.machine.add_transition(trigger='wake_up', source='asleep', dest='hanging out')

        # Superheroes need to keep in shape.
        self.machine.add_transition('work_out', 'hanging out', 'hungry')

        # Those calories won't replenish themselves!
        self.machine.add_transition('eat', 'hungry', 'hanging out')

        # Superheroes are always on call. ALWAYS. But they're not always
        # dressed in work-appropriate clothing.
        self.machine.add_transition('distress_call', '*', 'saving the world',
                         before='change_into_super_secret_costume')

        # When they get off work, they're all sweaty and disgusting. But before
        # they do anything else, they have to meticulously log their latest
        # escapades. Because the legal department says so.
        self.machine.add_transition('complete_mission', 'saving the world', 'sweaty',
                         after='update_journal')

        # Sweat is a disorder that can be remedied with water.
        # Unless you've had a particularly long day, in which case... bed time!
        self.machine.add_transition('clean_up', 'sweaty', 'asleep', conditions=['is_exhausted'])
        self.machine.add_transition('clean_up', 'sweaty', 'hanging out')

        # Our NarcolepticSuperhero can fall asleep at pretty much any time.
        self.machine.add_transition('nap', '*', 'asleep')

    def update_journal(self):
        """ Dear Diary, today I saved Mr. Whiskers. Again. """
        self.kittens_rescued += 1

    @property
    def is_exhausted(self):
        """ Basically a coin toss. """
        return random.random() < 0.5

    def change_into_super_secret_costume(self):
        print("Beauty, eh?")

现在,你已经将状态机烘焙到 NarcolepticSuperhero 中了。让我们带他/她/它出去转一圈...

>>> batman = NarcolepticSuperhero("Batman")
>>> batman.state
'asleep'

>>> batman.wake_up()
>>> batman.state
'hanging out'

>>> batman.nap()
>>> batman.state
'asleep'

>>> batman.clean_up()
MachineError: "Can't trigger event clean_up from state asleep!"

>>> batman.wake_up()
>>> batman.work_out()
>>> batman.state
'hungry'

# Batman still hasn't done anything useful...
>>> batman.kittens_rescued
0

# We now take you live to the scene of a horrific kitten entreement...
>>> batman.distress_call()
'Beauty, eh?'
>>> batman.state
'saving the world'

# Back to the crib.
>>> batman.complete_mission()
>>> batman.state
'sweaty'

>>> batman.clean_up()
>>> batman.state
'asleep'   # Too tired to shower!

# Another productive day, Alfred.
>>> batman.kittens_rescued
1

虽然我们无法读懂真正的蝙蝠侠的心思,但我们当然可以可视化我们的 NarcolepticSuperhero 的当前状态。

batman diagram

如果您想知道如何做到这一点,请查看 图表 扩展。

非快速入门

状态机是一种由有限数量的状态和这些状态之间的转换构成的行为模型。在每个状态和转换过程中,可以执行一些操作。状态机需要从一个初始状态开始。在使用转换时,状态机可能由多个对象组成,其中一些(机器)包含对其他(模型)进行操作的定义。下面,我们将探讨一些核心概念以及如何使用它们。

一些关键概念

  • 状态。状态表示状态机中的特定条件或阶段。它是过程中的一个独立的行为模式或阶段。

  • 转换。这是导致状态机从一个状态转换到另一个状态的过程或事件。

  • 模型。实际的状态化结构。它是转换过程中被更新的实体。它还可以定义在转换期间将执行的操作。例如,在转换之前、进入或离开状态时。

  • 机器。这是管理和控制模型、状态、转换和操作的实体。它是协调状态机整个过程的指挥者。

  • 触发器。这是启动转换的事件,它是发送信号以开始转换的方法。

  • 操作。在进入、离开特定状态或转换期间执行的具体操作或任务。操作通过回调实现,这些是当某些事件发生时执行的函数。

基本初始化

启动一个状态机相当简单。假设您有一个对象 lumpMatter 类的实例),并且您想管理其状态

class Matter(object):
    pass

lump = Matter()

您可以这样初始化一个(最小化)工作状态机,并将其绑定到模型 lump

from transitions import Machine
machine = Machine(model=lump, states=['solid', 'liquid', 'gas', 'plasma'], initial='solid')

# Lump now has a new state attribute!
lump.state
>>> 'solid'

另一种选择是不显式将模型传递给 Machine 初始化器

machine = Machine(states=['solid', 'liquid', 'gas', 'plasma'], initial='solid')

# The machine instance itself now acts as a model
machine.state
>>> 'solid'

请注意,这次我没有将 lump 模型作为参数传递。传递给 Machine 的第一个参数充当模型。因此,当我向其中传递一些内容时,所有便利函数都将添加到该对象中。如果没有提供模型,则 machine 实例本身充当模型。

当我最初说“最小化”时,是因为虽然这个状态机在技术上是可以运行的,但实际上并没有做任何事。它从 'solid' 状态开始,但不会进入另一个状态,因为没有定义转换……还没有!

让我们再试一次。

# The states
states=['solid', 'liquid', 'gas', 'plasma']

# And some transitions between states. We're lazy, so we'll leave out
# the inverse phase transitions (freezing, condensation, etc.).
transitions = [
    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid' },
    { 'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas' },
    { 'trigger': 'sublimate', 'source': 'solid', 'dest': 'gas' },
    { 'trigger': 'ionize', 'source': 'gas', 'dest': 'plasma' }
]

# Initialize
machine = Machine(lump, states=states, transitions=transitions, initial='liquid')

# Now lump maintains state...
lump.state
>>> 'liquid'

# And that state can change...
# Either calling the shiny new trigger methods
lump.evaporate()
lump.state
>>> 'gas'

# Or by calling the trigger method directly
lump.trigger('ionize')
lump.state
>>> 'plasma'

注意附加到 Matter 实例上的新方法(evaporate()ionize() 等)。每个方法都会触发相应的转换。转换也可以通过调用提供转换名称的 trigger() 方法来动态触发,如上所示。更多关于此的内容请参阅触发转换部分。

状态

任何良好的状态机的灵魂(无疑也包括许多不良状态机的灵魂)是一组状态。上面,我们通过向 Machine 初始化器传递字符串列表来定义有效的模型状态。但内部,状态实际上是表示为 State 对象。

您可以通过多种方式初始化和修改状态。具体来说,您可以

  • Machine 初始化器传递字符串,给出状态(的)名称,或者
  • 直接初始化每个新的 State 对象,或者
  • 传递包含初始化参数的字典

以下代码片段展示了实现同一目标的几种方法

# import Machine and State class
from transitions import Machine, State

# Create a list of 3 states to pass to the Machine
# initializer. We can mix types; in this case, we
# pass one State, one string, and one dict.
states = [
    State(name='solid'),
    'liquid',
    { 'name': 'gas'}
    ]
machine = Machine(lump, states)

# This alternative example illustrates more explicit
# addition of states and state callbacks, but the net
# result is identical to the above.
machine = Machine(lump)
solid = State('solid')
liquid = State('liquid')
gas = State('gas')
machine.add_states([solid, liquid, gas])

状态在添加到机器时初始化一次,并将持续存在,直到从其中移除。换句话说:如果您改变状态对象的属性,则下一次进入该状态时此更改不会被重置。如果您需要其他行为,请参阅如何扩展状态功能

回调

仅有状态和在这些状态间移动(转换)本身并不是非常有用。如果你想在进入或退出状态时执行某些操作,例如执行一个 动作,该怎么办呢?这时就需要用到 回调函数

一个 State 还可以关联一个 enterexit 回调函数的列表,这些函数会在状态机进入或离开该状态时被调用。你可以在初始化过程中通过将它们传递给 State 对象的构造函数、状态属性字典或稍后添加它们来指定回调函数。

为了方便起见,每当一个新的 State 被添加到 Machine 中时,会在 Machine(而不是模型!)上动态创建 on_enter_«state name»on_exit_«state name» 方法,这允许你在需要时动态添加新的进入和退出回调函数。

# Our old Matter class, now with  a couple of new methods we
# can trigger when entering or exit states.
class Matter(object):
    def say_hello(self): print("hello, new state!")
    def say_goodbye(self): print("goodbye, old state!")

lump = Matter()

# Same states as above, but now we give StateA an exit callback
states = [
    State(name='solid', on_exit=['say_goodbye']),
    'liquid',
    { 'name': 'gas', 'on_exit': ['say_goodbye']}
    ]

machine = Machine(lump, states=states)
machine.add_transition('sublimate', 'solid', 'gas')

# Callbacks can also be added after initialization using
# the dynamically added on_enter_ and on_exit_ methods.
# Note that the initial call to add the callback is made
# on the Machine and not on the model.
machine.on_enter_gas('say_hello')

# Test out the callbacks...
machine.set_state('solid')
lump.sublimate()
>>> 'goodbye, old state!'
>>> 'hello, new state!'

请注意,当 Machine 首次初始化时,on_enter_«state name» 回调函数 不会 被触发。例如,如果你定义了一个 on_enter_A() 回调函数,并使用 initial='A' 初始化 Machine,则 on_enter_A() 不会在进入状态 A 时触发。(如果你需要确保 on_enter_A() 在初始化时触发,你可以在 __init__ 方法中创建一个虚拟的初始状态,然后显式调用 to_A()。)

除了在初始化 State 时传递回调函数或在动态添加它们之外,你还可以在模型类本身中定义回调函数,这可能会提高代码的可读性。例如

class Matter(object):
    def say_hello(self): print("hello, new state!")
    def say_goodbye(self): print("goodbye, old state!")
    def on_enter_A(self): print("We've just entered state A!")

lump = Matter()
machine = Machine(lump, states=['A', 'B', 'C'])

现在,每当 lump 转换到状态 A 时,Matter 类中定义的 on_enter_A() 方法就会触发。

你可以使用 on_final 回调函数,当进入 final=True 的状态时,这些函数会被触发。

from transitions import Machine, State

states = [State(name='idling'),
          State(name='rescuing_kitten'),
          State(name='offender_gone', final=True),
          State(name='offender_caught', final=True)]

transitions = [["called", "idling", "rescuing_kitten"],  # we will come when  called
               {"trigger": "intervene",
                "source": "rescuing_kitten",
                "dest": "offender_gone",  # we
                "conditions": "offender_is_faster"},  # unless they are faster
               ["intervene", "rescuing_kitten", "offender_caught"]]


class FinalSuperhero(object):

    def __init__(self, speed):
        self.machine = Machine(self, states=states, transitions=transitions, initial="idling", on_final="claim_success")
        self.speed = speed

    def offender_is_faster(self, offender_speed):
        return self.speed < offender_speed

    def claim_success(self, **kwargs):
        print("The kitten is safe.")


hero = FinalSuperhero(speed=10)  # we are not in shape today
hero.called()
assert hero.is_rescuing_kitten()
hero.intervene(offender_speed=15)
# >>> 'The kitten is safe'
assert hero.machine.get_state(hero.state).final  # it's over
assert hero.is_offender_gone()  # maybe next time ...

检查状态

你可以通过以下方式检查模型的当前状态:

  • 检查 .state 属性,或者
  • 调用 is_«state name»()

如果你想要获取当前状态的 State 对象,可以通过 Machine 实例的 get_state() 方法来实现。

lump.state
>>> 'solid'
lump.is_gas()
>>> False
lump.is_solid()
>>> True
machine.get_state(lump.state).name
>>> 'solid'

如果你想要选择自己的状态属性名称,可以在初始化 Machine 时传递 model_attribute 参数。这将也会改变 is_«state name»() 的名称为 is_«model_attribute»_«state name»()。同样,自动转换的名称也将改为 to_«model_attribute»_«state name»() 而不是 to_«state name»()。这样做是为了允许多个机器在同一个模型上工作,并且具有不同的状态属性名称。

lump = Matter()
machine = Machine(lump, states=['solid', 'liquid', 'gas'],  model_attribute='matter_state', initial='solid')
lump.matter_state
>>> 'solid'
# with a custom 'model_attribute', states can also be checked like this:
lump.is_matter_state_solid()
>>> True
lump.to_matter_state_gas()
>>> True

枚举

到目前为止,我们已经看到如何为状态命名并使用这些名称来与我们的状态机交互。如果你更喜欢更严格的类型和更多的 IDE 代码补全(或者你再也不想打 'sesquipedalophobia' 因为这个词吓到你),使用 枚举 可能是你所需要的

import enum  # Python 2.7 users need to have 'enum34' installed
from transitions import Machine

class States(enum.Enum):
    ERROR = 0
    RED = 1
    YELLOW = 2
    GREEN = 3

transitions = [['proceed', States.RED, States.YELLOW],
               ['proceed', States.YELLOW, States.GREEN],
               ['error', '*', States.ERROR]]

m = Machine(states=States, transitions=transitions, initial=States.RED)
assert m.is_RED()
assert m.state is States.RED
state = m.get_state(States.RED)  # get transitions.State object
print(state.name)  # >>> RED
m.proceed()
m.proceed()
assert m.is_GREEN()
m.error()
assert m.state is States.ERROR

如果你喜欢,可以混合使用枚举和字符串(例如,[States.RED, 'ORANGE', States.YELLOW, States.GREEN]),但请注意,在内部,transitions 仍然通过名称(enum.Enum.name)来处理状态。因此,无法同时拥有状态 'GREEN'States.GREEN

转换

上述一些示例已经展示了转换的使用,但在这里我们将更详细地探讨它们。

与状态一样,每个转换在内部都表示为它自己的对象——Transition 类的实例。初始化一组转换的最快方法是向 Machine 初始化器传递字典或字典列表。我们已经在上面看到了这个例子

transitions = [
    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid' },
    { 'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas' },
    { 'trigger': 'sublimate', 'source': 'solid', 'dest': 'gas' },
    { 'trigger': 'ionize', 'source': 'gas', 'dest': 'plasma' }
]
machine = Machine(model=Matter(), states=states, transitions=transitions)

在字典中定义转换具有清晰度的优点,但可能会很繁琐。如果您追求简洁,可能会选择使用列表来定义转换。只需确保每个列表中的元素与Transition初始化中的位置参数(即triggersourcedestination等)顺序相同。

以下列表中的列表在功能上与上面的列表字典等效

transitions = [
    ['melt', 'solid', 'liquid'],
    ['evaporate', 'liquid', 'gas'],
    ['sublimate', 'solid', 'gas'],
    ['ionize', 'gas', 'plasma']
]

或者,您可以在初始化后向Machine添加转换

machine = Machine(model=lump, states=states, initial='solid')
machine.add_transition('melt', source='solid', dest='liquid')

触发转换

要执行转换,需要某些事件来触发它。有两种方法可以实现

  1. 使用基模型中自动附加的方法

    >>> lump.melt()
    >>> lump.state
    'liquid'
    >>> lump.evaporate()
    >>> lump.state
    'gas'
    

    注意您不需要在任何地方显式定义这些方法;每个转换的名称都绑定到传递给Machine初始化器的模型(在这种情况下,为lump)。这也意味着您的模型不应已经包含与事件触发器具有相同名称的方法,因为transitions将仅在位置未被占用时向您的模型附加便利方法。如果您想修改这种行为,请参阅常见问题解答

  2. 使用现在附加到您的模型(如果之前没有的话)的trigger方法。此方法允许您在需要动态触发时按名称执行转换

    >>> lump.trigger('melt')
    >>> lump.state
    'liquid'
    >>> lump.trigger('evaporate')
    >>> lump.state
    'gas'
    

触发无效的转换

默认情况下,触发无效转换将引发异常

>>> lump.to_gas()
>>> # This won't work because only objects in a solid state can melt
>>> lump.melt()
transitions.core.MachineError: "Can't trigger event melt from state gas!"

这种行为通常是期望的,因为它有助于提醒您代码中的问题。但在某些情况下,您可能希望默默地忽略无效触发器。您可以通过设置ignore_invalid_triggers=True(按状态逐个设置或全局对所有状态设置)来实现这一点

>>> # Globally suppress invalid trigger exceptions
>>> m = Machine(lump, states, initial='solid', ignore_invalid_triggers=True)
>>> # ...or suppress for only one group of states
>>> states = ['new_state1', 'new_state2']
>>> m.add_states(states, ignore_invalid_triggers=True)
>>> # ...or even just for a single state. Here, exceptions will only be suppressed when the current state is A.
>>> states = [State('A', ignore_invalid_triggers=True), 'B', 'C']
>>> m = Machine(lump, states)
>>> # ...this can be inverted as well if just one state should raise an exception
>>> # since the machine's global value is not applied to a previously initialized state.
>>> states = ['A', 'B', State('C')] # the default value for 'ignore_invalid_triggers' is False
>>> m = Machine(lump, states, ignore_invalid_triggers=True)

如果您需要知道从某个状态出发哪些转换是有效的,您可以使用get_triggers

m.get_triggers('solid')
>>> ['melt', 'sublimate']
m.get_triggers('liquid')
>>> ['evaporate']
m.get_triggers('plasma')
>>> []
# you can also query several states at once
m.get_triggers('solid', 'liquid', 'gas', 'plasma')
>>> ['melt', 'evaporate', 'sublimate', 'ionize']

如果您从开始就遵循了这份文档,您会注意到get_triggers实际上返回了比上面显式定义的更多触发器,例如to_liquid等。这些被称为自动转换,将在下一节中介绍。

对所有状态的自动转换

除了显式添加的任何转换外,每当向Machine实例添加一个状态时,都会自动创建一个to_«state»()方法。此方法将过渡到目标状态,无论机器当前处于哪个状态

lump.to_liquid()
lump.state
>>> 'liquid'
lump.to_solid()
lump.state
>>> 'solid'

如果您希望禁用此行为,可以在Machine初始化器中将auto_transitions=False设置

从多个状态进行转换

给定的触发器可以附加到多个转换上,其中一些可能开始或结束于相同的状态。例如

machine.add_transition('transmogrify', ['solid', 'liquid', 'gas'], 'plasma')
machine.add_transition('transmogrify', 'plasma', 'solid')
# This next transition will never execute
machine.add_transition('transmogrify', 'plasma', 'gas')

在这种情况下,调用transmogrify()会将模型的状态设置为'solid',如果它当前是'plasma',否则将其设置为'plasma'。(请注意,只有第一个匹配的转换将执行;因此,上面最后一行定义的转换不会做任何事情。)

您也可以使用'*'通配符使触发器从所有状态转换到特定的目标状态

machine.add_transition('to_liquid', '*', 'liquid')

请注意,通配符转换仅适用于在调用add_transition()时存在的状态。当模型处于在转换定义之后添加的状态时调用基于通配符的转换将引发无效转换消息,并且不会转换到目标状态。

从多个状态的反射性转换

可以通过指定=作为目标来轻松添加具有相同状态作为源和目标的反射性触发器。如果您想将相同的反射性触发器添加到多个状态,这会很有用。例如

machine.add_transition('touch', ['liquid', 'gas', 'plasma'], '=', after='change_shape')

这将添加三个状态的反身转换,以touch()作为触发器,并在每次触发后执行change_shape

内部转换

与反身转换不同,内部转换永远不会真正离开状态。这意味着在状态相关的回调exitenter将不会处理时,将处理转换相关的回调beforeafter。要定义一个内部转换,请将目标设置为None

machine.add_transition('internal', ['liquid', 'gas'], None, after='change_shape')

有序转换

人们通常希望状态转换遵循严格的线性序列。例如,给定状态['A', 'B', 'C'],你可能希望对ABBCCA(但不是其他对)进行有效转换。

为了实现这种行为,Transitions在Machine类中提供了一个add_ordered_transitions()方法。

states = ['A', 'B', 'C']
 # See the "alternative initialization" section for an explanation of the 1st argument to init
machine = Machine(states=states, initial='A')
machine.add_ordered_transitions()
machine.next_state()
print(machine.state)
>>> 'B'
# We can also define a different order of transitions
machine = Machine(states=states, initial='A')
machine.add_ordered_transitions(['A', 'C', 'B'])
machine.next_state()
print(machine.state)
>>> 'C'
# Conditions can be passed to 'add_ordered_transitions' as well
# If one condition is passed, it will be used for all transitions
machine = Machine(states=states, initial='A')
machine.add_ordered_transitions(conditions='check')
# If a list is passed, it must contain exactly as many elements as the
# machine contains states (A->B, ..., X->A)
machine = Machine(states=states, initial='A')
machine.add_ordered_transitions(conditions=['check_A2B', ..., 'check_X2A'])
# Conditions are always applied starting from the initial state
machine = Machine(states=states, initial='B')
machine.add_ordered_transitions(conditions=['check_B2C', ..., 'check_A2B'])
# With `loop=False`, the transition from the last state to the first state will be omitted (e.g. C->A)
# When you also pass conditions, you need to pass one condition less (len(states)-1)
machine = Machine(states=states, initial='A')
machine.add_ordered_transitions(loop=False)
machine.next_state()
machine.next_state()
machine.next_state() # transitions.core.MachineError: "Can't trigger event next_state from state C!"

队列转换

在Transitions中,默认行为是立即处理事件。这意味着在on_enter方法内的事件将在绑定到after的回调被调用之前处理。

def go_to_C():
    global machine
    machine.to_C()

def after_advance():
    print("I am in state B now!")

def entering_C():
    print("I am in state C now!")

states = ['A', 'B', 'C']
machine = Machine(states=states, initial='A')

# we want a message when state transition to B has been completed
machine.add_transition('advance', 'A', 'B', after=after_advance)

# call transition from state B to state C
machine.on_enter_B(go_to_C)

# we also want a message when entering state C
machine.on_enter_C(entering_C)
machine.advance()
>>> 'I am in state C now!'
>>> 'I am in state B now!' # what?

此示例的执行顺序是

prepare -> before -> on_enter_B -> on_enter_C -> after.

如果启用了队列处理,则转换将在触发下一个转换之前完成。

machine = Machine(states=states, queued=True, initial='A')
...
machine.advance()
>>> 'I am in state B now!'
>>> 'I am in state C now!' # That's better!

这导致

prepare -> before -> on_enter_B -> queue(to_C) -> after  -> on_enter_C.

重要提示:在队列中处理事件时,触发调用将始终返回True,因为在排队时无法确定涉及排队调用的转换最终是否成功完成。即使只处理单个事件,也是如此。

machine.add_transition('jump', 'A', 'C', conditions='will_fail')
...
# queued=False
machine.jump()
>>> False
# queued=True
machine.jump()
>>> True

当模型从机器中移除时,transitions也将从队列中删除所有相关事件。

class Model:
    def on_enter_B(self):
        self.to_C()  # add event to queue ...
        self.machine.remove_model(self)  # aaaand it's gone

条件转换

有时你只想在特定条件发生时执行特定转换。你可以通过传递方法或方法列表到conditions参数来实现这一点。

# Our Matter class, now with a bunch of methods that return booleans.
class Matter(object):
    def is_flammable(self): return False
    def is_really_hot(self): return True

machine.add_transition('heat', 'solid', 'gas', conditions='is_flammable')
machine.add_transition('heat', 'solid', 'liquid', conditions=['is_really_hot'])

在上面的示例中,当模型处于'solid'状态时调用heat(),如果is_flammable返回True,则将转换到状态'gas'。否则,如果is_really_hot返回True,则将转换到状态'liquid'

为了方便,还有一个'unless'参数,其行为与条件相同,但相反。

machine.add_transition('heat', 'solid', 'gas', unless=['is_flammable', 'is_really_hot'])

在这种情况下,如果is_flammable()is_really_hot()都返回False,则每次触发heat()时,模型将从不稳定的固态转换为气态。

请注意,条件检查方法将被动接收触发方法传递的可选参数和数据对象。例如,以下调用

lump.heat(temp=74)
# equivalent to lump.trigger('heat', temp=74)

... 将将可选参数temp=74传递给is_flammable()检查(可能被EventData实例包装)。有关更多信息,请参阅下面的传递数据部分。

检查转换

如果你想确保在继续之前转换是可能的,你可以使用添加到你的模型的may_<trigger_name>函数。你的模型还包含用于按名称检查触发器的may_trigger函数。

# check if the current temperature is hot enough to trigger a transition
if lump.may_heat():
# if lump.may_trigger("heat"):
    lump.heat()

这将执行所有prepare回调并评估分配给潜在转换的条件。转换检查还可以用于转换的目标尚未可用时。

machine.add_transition('elevate', 'solid', 'spiritual')
assert not lump.may_elevate()  # not ready yet :(
assert not lump.may_trigger("elevate")  # same result for checks via trigger name

回调

你还可以将回调附加到转换以及状态。每个转换都有'before''after'属性,包含在转换执行前后要调用的方法的列表。

class Matter(object):
    def make_hissing_noises(self): print("HISSSSSSSSSSSSSSSS")
    def disappear(self): print("where'd all the liquid go?")

transitions = [
    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid', 'before': 'make_hissing_noises'},
    { 'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas', 'after': 'disappear' }
]

lump = Matter()
machine = Machine(lump, states, transitions=transitions, initial='solid')
lump.melt()
>>> "HISSSSSSSSSSSSSSSS"
lump.evaporate()
>>> "where'd all the liquid go?"

还有一个'prepare'回调,在转换开始时执行,在检查任何'conditions'或其他回调之前。

class Matter(object):
    heat = False
    attempts = 0
    def count_attempts(self): self.attempts += 1
    def heat_up(self): self.heat = random.random() < 0.25
    def stats(self): print('It took you %i attempts to melt the lump!' %self.attempts)

    @property
    def is_really_hot(self):
        return self.heat


states=['solid', 'liquid', 'gas', 'plasma']

transitions = [
    { 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid', 'prepare': ['heat_up', 'count_attempts'], 'conditions': 'is_really_hot', 'after': 'stats'},
]

lump = Matter()
machine = Machine(lump, states, transitions=transitions, initial='solid')
lump.melt()
lump.melt()
lump.melt()
lump.melt()
>>> "It took you 4 attempts to melt the lump!"

请注意,除非当前状态是命名转换的有效源,否则将不会调用prepare

可以在初始化时将默认操作传递给Machine,分别使用before_state_changeafter_state_change

class Matter(object):
    def make_hissing_noises(self): print("HISSSSSSSSSSSSSSSS")
    def disappear(self): print("where'd all the liquid go?")

states=['solid', 'liquid', 'gas', 'plasma']

lump = Matter()
m = Machine(lump, states, before_state_change='make_hissing_noises', after_state_change='disappear')
lump.to_gas()
>>> "HISSSSSSSSSSSSSSSS"
>>> "where'd all the liquid go?"

还有两个用于回调的关键字,这些回调应独立执行,a) 不论可能有多少个转换,b) 如果有任何转换成功,以及 c) 即使在执行其他回调期间发生错误。传递给Machine的具有prepare_event的回调将在处理可能的转换(及其各自的prepare回调)之前仅执行一次。无论处理的转换是否成功,finalize_event的回调都将执行。注意,如果发生错误,它将以error的形式附加到event_data上,并且可以通过send_event=True检索。

from transitions import Machine

class Matter(object):
    def raise_error(self, event): raise ValueError("Oh no")
    def prepare(self, event): print("I am ready!")
    def finalize(self, event): print("Result: ", type(event.error), event.error)

states=['solid', 'liquid', 'gas', 'plasma']

lump = Matter()
m = Machine(lump, states, prepare_event='prepare', before_state_change='raise_error',
            finalize_event='finalize', send_event=True)
try:
    lump.to_gas()
except ValueError:
    pass
print(lump.state)

# >>> I am ready!
# >>> Result:  <class 'ValueError'> Oh no
# >>> initial

有时事情并不像预期的那样顺利,我们需要处理异常并清理混乱以保持事情继续进行。我们可以传递回调到on_exception来执行此操作

from transitions import Machine

class Matter(object):
    def raise_error(self, event): raise ValueError("Oh no")
    def handle_error(self, event):
        print("Fixing things ...")
        del event.error  # it did not happen if we cannot see it ...

states=['solid', 'liquid', 'gas', 'plasma']

lump = Matter()
m = Machine(lump, states, before_state_change='raise_error', on_exception='handle_error', send_event=True)
try:
    lump.to_gas()
except ValueError:
    pass
print(lump.state)

# >>> Fixing things ...
# >>> initial

可调用解析

正如你可能已经意识到的,将可调用传递给状态、条件和转换的标准方式是通过名称。在处理回调和条件时,transitions将使用它们的名称从模型中检索相关的可调用项。如果无法检索方法并且它包含点,则transitions将把名称视为模块函数的路径,并尝试导入它。或者,你可以传递属性或属性的名称。它们将被包装成函数,但不能接收事件数据,这是显而易见的。你还可以直接传递(绑定)函数等可调用项。如前所述,你还可以将可调用项名称的列表/元组传递给回调参数。回调将按添加的顺序执行。

from transitions import Machine
from mod import imported_func

import random


class Model(object):

    def a_callback(self):
        imported_func()

    @property
    def a_property(self):
        """ Basically a coin toss. """
        return random.random() < 0.5

    an_attribute = False


model = Model()
machine = Machine(model=model, states=['A'], initial='A')
machine.add_transition('by_name', 'A', 'A', conditions='a_property', after='a_callback')
machine.add_transition('by_reference', 'A', 'A', unless=['a_property', 'an_attribute'], after=model.a_callback)
machine.add_transition('imported', 'A', 'A', after='mod.imported_func')

model.by_name()
model.by_reference()
model.imported()

可调用解析在Machine.resolve_callable中完成。如果需要更复杂的可调用解析策略,则可以覆盖此方法。

示例

class CustomMachine(Machine):
    @staticmethod
    def resolve_callable(func, event_data):
        # manipulate arguments here and return func, or super() if no manipulation is done.
        super(CustomMachine, CustomMachine).resolve_callable(func, event_data)

回调执行顺序

总的来说,目前有三种触发事件的方法。你可以调用模型的便利函数,如lump.melt(),通过名称执行触发器,如lump.trigger("melt"),或使用machine.dispatch("melt")在多个模型上分发事件(请参阅替代初始化模式部分中的多个模型部分)。然后,转换上的回调将按以下顺序执行

回调 当前状态 注释
'machine.prepare_event' 在处理单个转换之前仅执行一次
'transition.prepare' 转换开始时立即执行
'transition.conditions' 条件可能失败并停止转换
'transition.unless' 条件可能失败并停止转换
'machine.before_state_change' 在模型上声明的默认回调
'transition.before'
'state.on_exit' 在源状态下声明的回调
<状态更改>
'state.on_enter' 目标 在目标状态下声明的回调
'transition.after' 目标
'machine.on_final' 目标 将首先调用子级上的回调
'machine.after_state_change' 目标 在模型上声明的默认回调;在内联转换之后也会调用
'machine.on_exception' 源/目标 当引发异常时将执行回调
'machine.finalize_event' 源/目标 即使没有发生转换或引发异常,也会执行回调

如果任何回调抛出异常,则不会继续处理回调。这意味着在转换之前(在 state.on_exit 或更早)发生错误时,它会停止。如果在转换之后(在 state.on_enter 或之后)抛出异常,状态变化将保持不变,并且不会发生回滚。除非最终化回调本身抛出异常,否则在 machine.finalize_event 中指定的回调将始终执行。请注意,每个回调序列必须在执行下一阶段之前完成。阻塞回调将阻止执行顺序,因此会阻塞 triggerdispatch 调用本身。如果您希望回调并行执行,可以查看扩展的 extensions AsyncMachine 以进行异步处理,或 LockedMachine 以进行多线程。

传递数据

有时您需要在机器初始化时将反映模型当前状态的一些数据传递给注册的回调函数。转换允许您以两种不同的方式完成此操作。

首先(默认方式),您可以直接将任何位置参数或关键字参数传递给触发方法(当您调用 add_transition() 时创建)

class Matter(object):
    def __init__(self): self.set_environment()
    def set_environment(self, temp=0, pressure=101.325):
        self.temp = temp
        self.pressure = pressure
    def print_temperature(self): print("Current temperature is %d degrees celsius." % self.temp)
    def print_pressure(self): print("Current pressure is %.2f kPa." % self.pressure)

lump = Matter()
machine = Machine(lump, ['solid', 'liquid'], initial='solid')
machine.add_transition('melt', 'solid', 'liquid', before='set_environment')

lump.melt(45)  # positional arg;
# equivalent to lump.trigger('melt', 45)
lump.print_temperature()
>>> 'Current temperature is 45 degrees celsius.'

machine.set_state('solid')  # reset state so we can melt again
lump.melt(pressure=300.23)  # keyword args also work
lump.print_pressure()
>>> 'Current pressure is 300.23 kPa.'

您可以向触发器传递任意数量的参数。

这种方法有一个重要的限制:每个由状态转换触发的回调函数都必须能够处理 所有 参数。如果每个回调都期望不同的数据,这可能会导致问题。

为了解决这个问题,转换支持发送数据的另一种方法。如果您在 Machine 初始化时设置 send_event=True,则所有触发器的参数都将被包装在 EventData 实例中,并传递给每个回调。(EventData 对象还维护对相关事件的源状态、模型、转换、机器和触发器的内部引用,以防您需要访问这些信息。)

class Matter(object):

    def __init__(self):
        self.temp = 0
        self.pressure = 101.325

    # Note that the sole argument is now the EventData instance.
    # This object stores positional arguments passed to the trigger method in the
    # .args property, and stores keywords arguments in the .kwargs dictionary.
    def set_environment(self, event):
        self.temp = event.kwargs.get('temp', 0)
        self.pressure = event.kwargs.get('pressure', 101.325)

    def print_pressure(self): print("Current pressure is %.2f kPa." % self.pressure)

lump = Matter()
machine = Machine(lump, ['solid', 'liquid'], send_event=True, initial='solid')
machine.add_transition('melt', 'solid', 'liquid', before='set_environment')

lump.melt(temp=45, pressure=1853.68)  # keyword args
lump.print_pressure()
>>> 'Current pressure is 1853.68 kPa.'

替代初始化模式

到目前为止的所有示例中,我们都将一个新的 Machine 实例附加到一个单独的模型(lumpMatter 类的一个实例)上。虽然这种分离可以使事物保持整洁(因为您不必将许多新方法猴子补丁到 Matter 类中),但它也可能有点烦恼,因为它需要您跟踪在状态机上调用的哪些方法,以及绑定到状态机的模型上调用的哪些方法(例如,lump.on_enter_StateA()machine.add_transition())。

幸运的是,转换很灵活,并支持两种其他初始化模式。

首先,您可以创建一个不需要任何其他模型的独立状态机。只需在初始化时省略模型参数

machine = Machine(states=states, transitions=transitions, initial='solid')
machine.melt()
machine.state
>>> 'liquid'

如果您以这种方式初始化机器,则可以将所有触发事件(如 evaporate()sublimate() 等)和所有回调函数直接附加到 Machine 实例。

这种方法的好处是将所有状态机功能集中在一个地方,但如果您认为状态逻辑应该包含在模型本身中而不是在单独的控制台中,可能会感觉有点不自然。

另一种(可能更好的)方法是让模型继承自 Machine 类。转换旨在无缝支持继承。(只需确保重写类 Machine__init__ 方法!)

class Matter(Machine):
    def say_hello(self): print("hello, new state!")
    def say_goodbye(self): print("goodbye, old state!")

    def __init__(self):
        states = ['solid', 'liquid', 'gas']
        Machine.__init__(self, states=states, initial='solid')
        self.add_transition('melt', 'solid', 'liquid')

lump = Matter()
lump.state
>>> 'solid'
lump.melt()
lump.state
>>> 'liquid'

在这里,您可以将所有状态机功能整合到现有的模型中,这通常比将所有功能都放在单独的独立 Machine 实例中感觉更自然。

机器可以处理多个模型,可以通过列表的形式传递,例如 Machine(model=[model1, model2, ...])。在需要同时添加模型和机器实例本身的情况下,可以在初始化时传递类变量占位符(字符串)Machine.self_literal,如 Machine(model=[Machine.self_literal, model1, ...])。您还可以创建一个独立的机器,并通过传递 model=None 给构造函数,通过 machine.add_model 动态注册模型。此外,您还可以使用 machine.dispatch 触发所有已添加模型的事件。请记住,如果机器是长期运行的,而您的模型是临时的并且应该被垃圾回收,请调用 machine.remove_model

class Matter():
    pass

lump1 = Matter()
lump2 = Matter()

# setting 'model' to None or passing an empty list will initialize the machine without a model
machine = Machine(model=None, states=states, transitions=transitions, initial='solid')

machine.add_model(lump1)
machine.add_model(lump2, initial='liquid')

lump1.state
>>> 'solid'
lump2.state
>>> 'liquid'

# custom events as well as auto transitions can be dispatched to all models
machine.dispatch("to_plasma")

lump1.state
>>> 'plasma'
assert lump1.state == lump2.state

machine.remove_model([lump1, lump2])
del lump1  # lump1 is garbage collected
del lump2  # lump2 is garbage collected

如果在状态机构造函数中不提供初始状态,transitions 将创建并添加一个默认状态,称为 'initial'。如果您不希望有默认初始状态,可以传递 initial=None。然而,在这种情况下,每次添加模型时都需要传递一个初始状态。

machine = Machine(model=None, states=states, transitions=transitions, initial=None)

machine.add_model(Matter())
>>> "MachineError: No initial state configured for machine, must specify when adding model."
machine.add_model(Matter(), initial='liquid')

具有多个状态的模式可以使用不同的 model_attribute 值附加多个机器。如 检查状态 中所述,这将添加自定义的 is/to_<model_attribute>_<state_name> 函数。

lump = Matter()

matter_machine = Machine(lump, states=['solid', 'liquid', 'gas'], initial='solid')
# add a second machine to the same model but assign a different state attribute
shipment_machine = Machine(lump, states=['delivered', 'shipping'], initial='delivered', model_attribute='shipping_state')

lump.state
>>> 'solid'
lump.is_solid()  # check the default field
>>> True
lump.shipping_state
>>> 'delivered'
lump.is_shipping_state_delivered()  # check the custom field.
>>> True
lump.to_shipping_state_shipping()
>>> True
lump.is_shipping_state_delivered()
>>> False

日志记录

transitions 包含非常基础的日志记录功能。一些事件(即状态变化、转换触发和条件检查)被记录为使用标准 Python logging 模块的信息级别事件。这意味着您可以在脚本中轻松配置日志记录到标准输出。

# Set up logging; The basic log level will be DEBUG
import logging
logging.basicConfig(level=logging.DEBUG)
# Set transitions' log level to INFO; DEBUG messages will be omitted
logging.getLogger('transitions').setLevel(logging.INFO)

# Business as usual
machine = Machine(states=states, transitions=transitions, initial='solid')
...

(重新)存储机器实例

机器是可序列化的,可以使用 pickle 进行存储和加载。对于 Python 3.3 及更早版本,需要 dill

import dill as pickle # only required for Python 3.3 and earlier

m = Machine(states=['A', 'B', 'C'], initial='A')
m.to_B()
m.state
>>> B

# store the machine
dump = pickle.dumps(m)

# load the Machine instance again
m2 = pickle.loads(dump)

m2.state
>>> B

m2.states.keys()
>>> ['A', 'B', 'C']

类型支持

如您所注意到的,transitions 使用了 Python 的一些动态特性,以为您提供处理模型的手动方法。然而,静态类型检查器不喜欢模型属性和方法在运行时之前未知。历史上,transitions 也未将已定义在模型上的便利方法分配给模型,以防止意外的覆盖。

但请放心!您可以使用机器构造函数参数 model_override 来更改模型的装饰方式。如果您设置 model_override=Truetransitions 将仅覆盖已定义的方法。这可以防止在运行时出现新的方法,并允许您定义您想要使用的辅助方法。

from transitions import Machine

# Dynamic assignment
class Model:
    pass

model = Model()
default_machine = Machine(model, states=["A", "B"], transitions=[["go", "A", "B"]], initial="A")
print(model.__dict__.keys())  # all convenience functions have been assigned
# >> dict_keys(['trigger', 'to_A', 'may_to_A', 'to_B', 'may_to_B', 'go', 'may_go', 'is_A', 'is_B', 'state'])
assert model.is_A()  # Unresolved attribute reference 'is_A' for class 'Model'


# Predefined assigment: We are just interested in calling our 'go' event and will trigger the other events by name
class PredefinedModel:
    # state (or another parameter if you set 'model_attribute') will be assigned anyway 
    # because we need to keep track of the model's state
    state: str

    def go(self) -> bool:
        raise RuntimeError("Should be overridden!")

    def trigger(self, trigger_name: str) -> bool:
        raise RuntimeError("Should be overridden!")


model = PredefinedModel()
override_machine = Machine(model, states=["A", "B"], transitions=[["go", "A", "B"]], initial="A", model_override=True)
print(model.__dict__.keys())
# >> dict_keys(['trigger', 'go', 'state'])
model.trigger("to_B")
assert model.state == "B"

如果您想使用所有便利函数并将一些回调函数混合在一起,当您有很多状态和转换定义时,定义模型可能会变得相当复杂。在 transitions 中的 generate_base_model 方法可以从机器配置生成基本模型,以帮助您处理。

from transitions.experimental.utils import generate_base_model
simple_config = {
    "states": ["A", "B"],
    "transitions": [
        ["go", "A", "B"],
    ],
    "initial": "A",
    "before_state_change": "call_this",
    "model_override": True,
} 

class_definition = generate_base_model(simple_config)
with open("base_model.py", "w") as f:
    f.write(class_definition)

# ... in another file
from transitions import Machine
from base_model import BaseModel

class Model(BaseModel):  #  call_this will be an abstract method in BaseModel

    def call_this(self) -> None:
        # do something  

model = Model()
machine = Machine(model, **simple_config)

定义将被覆盖的模型方法会增加一些额外的工作。在状态和转换在模型之前或之后定义在列表中时,切换回和 forth 以确保事件名称拼写正确可能会很麻烦。您可以通过将状态定义为枚举来减少样板代码和工作中的不确定性。您还可以在模型类中使用 add_transitionsevent 来直接定义转换。您是否使用函数装饰器 add_transitions 或事件来分配值取决于您的代码风格偏好。它们都以相同的方式工作,具有相同的签名,并应产生(几乎)相同的 IDE 类型提示。由于这仍是一个正在进行的工作,您需要创建一个自定义的 Machine 类,并使用 with_model_definitions 对转换进行检查。

from enum import Enum

from transitions.experimental.utils import with_model_definitions, event, add_transitions, transition
from transitions import Machine


class State(Enum):
    A = "A"
    B = "B"
    C = "C"


class Model:

    state: State = State.A

    @add_transitions(transition(source=State.A, dest=State.B), [State.C, State.A])
    @add_transitions({"source": State.B,  "dest": State.A})
    def foo(self): ...

    bar = event(
        {"source": State.B, "dest": State.A, "conditions": lambda: False},
        transition(source=State.B, dest=State.C)
    )


@with_model_definitions  # don't forget to define your model with this decorator!
class MyMachine(Machine):
    pass


model = Model()
machine = MyMachine(model, states=State, initial=model.state)
model.foo()
model.bar()
assert model.state == State.C
model.foo()
assert model.state == State.A

扩展

尽管 transitions 的核心保持轻量级,但有多种 MixIns 可以扩展其功能。目前支持的有

  • 分层状态机 用于嵌套和重用
  • 图示 用于可视化机器的当前状态
  • 线程安全锁 用于并行执行
  • 异步回调 用于异步执行
  • 自定义状态 用于扩展状态相关行为

有两种机制可以获取具有所需功能的有限状态机实例。第一种方法是使用方便的 factory,其中四个参数 graphnestedlockedasyncio 设置为 True 以启用所需功能。

from transitions.extensions import MachineFactory

# create a machine with mixins
diagram_cls = MachineFactory.get_predefined(graph=True)
nested_locked_cls = MachineFactory.get_predefined(nested=True, locked=True)
async_machine_cls = MachineFactory.get_predefined(asyncio=True)

# create instances from these classes
# instances can be used like simple machines
machine1 = diagram_cls(model, state, transitions)
machine2 = nested_locked_cls(model, state, transitions)

这种方法针对实验性使用,因为在这种情况下,不需要了解底层类。但是,也可以直接从 transitions.extensions 中导入类。命名方案如下:

图示 嵌套 锁定 异步IO
机器
GraphMachine
HierarchicalMachine
LockedMachine
HierarchicalGraphMachine
LockedGraphMachine
LockedHierarchicalMachine
LockedHierarchicalGraphMachine
AsyncMachine
AsyncGraphMachine
HierarchicalAsyncMachine
HierarchicalAsyncGraphMachine

要使用功能丰富的状态机,可以编写

from transitions.extensions import LockedHierarchicalGraphMachine as LHGMachine

machine = LHGMachine(model, states, transitions)

分层状态机 (HSM)

Transitions 包含一个扩展模块,允许嵌套状态。这允许我们创建上下文,并模拟状态与状态机中某些子任务相关的场景。要创建嵌套状态,可以从 transitions 导入 NestedState 或使用带有初始化参数 namechildren 的字典。可选地,可以使用 initial 定义当进入嵌套状态时要转换到的子状态。

from transitions.extensions import HierarchicalMachine

states = ['standing', 'walking', {'name': 'caffeinated', 'children':['dithering', 'running']}]
transitions = [
  ['walk', 'standing', 'walking'],
  ['stop', 'walking', 'standing'],
  ['drink', '*', 'caffeinated'],
  ['walk', ['caffeinated', 'caffeinated_dithering'], 'caffeinated_running'],
  ['relax', 'caffeinated', 'standing']
]

machine = HierarchicalMachine(states=states, transitions=transitions, initial='standing', ignore_invalid_triggers=True)

machine.walk() # Walking now
machine.stop() # let's stop for a moment
machine.drink() # coffee time
machine.state
>>> 'caffeinated'
machine.walk() # we have to go faster
machine.state
>>> 'caffeinated_running'
machine.stop() # can't stop moving!
machine.state
>>> 'caffeinated_running'
machine.relax() # leave nested state
machine.state # phew, what a ride
>>> 'standing'
# machine.on_enter_caffeinated_running('callback_method')

一个使用 initial 的配置可能如下所示:

# ...
states = ['standing', 'walking', {'name': 'caffeinated', 'initial': 'dithering', 'children': ['dithering', 'running']}]
transitions = [
  ['walk', 'standing', 'walking'],
  ['stop', 'walking', 'standing'],
  # this transition will end in 'caffeinated_dithering'...
  ['drink', '*', 'caffeinated'],
  # ... that is why we do not need do specify 'caffeinated' here anymore
  ['walk', 'caffeinated_dithering', 'caffeinated_running'],
  ['relax', 'caffeinated', 'standing']
]
# ...

initial 关键字是 HierarchicalMachine 构造函数的参数,它接受嵌套状态(例如,initial='caffeinated_running')和被视为并行状态的状态列表(例如,initial=['A', 'B'])或另一个模型的状态(例如,initial=model.state),这应该是上述选项之一。注意,当传递字符串时,transition 将检查目标状态的 initial 子状态,并将其用作入口状态。这将递归地进行,直到子状态不提及初始状态。并行状态或作为列表传递的状态将“按原样”使用,并且不会进行进一步的初始评估。

请注意,您之前创建的状态对象必须是一个 NestedState 或其派生类。在简单的 Machine 实例中使用的标准 State 类缺少用于嵌套所需的功能。

from transitions.extensions.nesting import HierarchicalMachine, NestedState
from transitions import  State
m = HierarchicalMachine(states=['A'], initial='initial')
m.add_state('B')  # fine
m.add_state({'name': 'C'})  # also fine
m.add_state(NestedState('D'))  # fine as well
m.add_state(State('E'))  # does not work!

在使用嵌套状态时需要考虑的一些事项:状态 名称与 NestedState.separator 连接。目前分隔符设置为下划线 ('_'),因此与基本机器的行为类似。这意味着从状态 foo 出来的子状态 bar 将由 foo_bar 知道。bar 的子状态 baz 将被称为 foo_bar_baz,依此类推。进入子状态时,将调用所有父状态的 enter。对于退出子状态也是如此。第三,嵌套状态可以覆盖其父状态的转换行为。如果当前状态不知道转换,它将被委托给其父状态。

这意味着在标准配置中,HSM(Hierarchical State Machine)中的状态名称不得包含下划线。对于transitions来说,无法判断machine.add_state('state_name')是应该添加一个名为state_name的状态,还是向状态state添加一个名为name的子状态。但在某些情况下,这并不足够。例如,如果状态名称由多个单词组成,并且您需要使用下划线来分隔它们而不是CamelCase。为了处理这种情况,您可以非常容易地更改分隔符字符。如果您使用Python 3,甚至可以使用花哨的Unicode字符。将分隔符设置为不是下划线会改变一些行为(自动转换和设置回调)。

from transitions.extensions import HierarchicalMachine
from transitions.extensions.nesting import NestedState
NestedState.separator = '↦'
states = ['A', 'B',
  {'name': 'C', 'children':['1', '2',
    {'name': '3', 'children': ['a', 'b', 'c']}
  ]}
]

transitions = [
    ['reset', 'C', 'A'],
    ['reset', 'C↦2', 'C']  # overwriting parent reset
]

# we rely on auto transitions
machine = HierarchicalMachine(states=states, transitions=transitions, initial='A')
machine.to_B()  # exit state A, enter state B
machine.to_C()  # exit B, enter C
machine.to_C.s3.a()  # enter C↦a; enter C↦3↦a;
machine.state
>>> 'C↦3↦a'
assert machine.is_C.s3.a()
machine.to('C↦2')  # not interactive; exit C↦3↦a, exit C↦3, enter C↦2
machine.reset()  # exit C↦2; reset C has been overwritten by C↦3
machine.state
>>> 'C'
machine.reset()  # exit C, enter A
machine.state
>>> 'A'
# s.on_enter('C↦3↦a', 'callback_method')

自动转换不再是to_C_3_a(),而是调用为to_C.s3.a()。如果您的子状态以数字开头,转换将添加前缀's'('3'变成's3')到自动转换的FunctionWrapper,以符合Python的属性命名方案。如果不需要交互式完成,可以直接调用to('C↦3↦a')。此外,on_enter/exit_<<state name>>被替换为on_enter/exit(state_name, callback)。状态检查也可以以类似的方式进行。而不是is_C_3_a(),可以使用FunctionWrapper的变体is_C.s3.a()

要检查当前状态是否是特定状态的下级状态,is_state支持关键字allow_substates

machine.state
>>> 'C.2.a'
machine.is_C() # checks for specific states
>>> False
machine.is_C(allow_substates=True)
>>> True
assert machine.is_C.s2() is False
assert machine.is_C.s2(allow_substates=True)  # FunctionWrapper support allow_substate as well

您还可以在HSM中使用枚举,但请记住,枚举是通过值来比较的。如果在状态树中有重复的值,则无法区分这些状态。

states = [States.RED, States.YELLOW, {'name': States.GREEN, 'children': ['tick', 'tock']}]
states = ['A', {'name': 'B', 'children': states, 'initial': States.GREEN}, States.GREEN]
machine = HierarchicalMachine(states=states)
machine.to_B()
machine.is_GREEN()  # returns True even though the actual state is B_GREEN

HierarchicalMachine已从头开始重写,以支持并行状态和嵌套状态的更好隔离。这涉及到基于社区反馈的一些调整。要了解处理顺序和配置,请查看以下示例

from transitions.extensions.nesting import HierarchicalMachine
import logging
states = ['A', 'B', {'name': 'C', 'parallel': [{'name': '1', 'children': ['a', 'b', 'c'], 'initial': 'a',
                                                'transitions': [['go', 'a', 'b']]},
                                               {'name': '2', 'children': ['x', 'y', 'z'], 'initial': 'z'}],
                      'transitions': [['go', '2_z', '2_x']]}]

transitions = [['reset', 'C_1_b', 'B']]
logging.basicConfig(level=logging.INFO)
machine = HierarchicalMachine(states=states, transitions=transitions, initial='A')
machine.to_C()
# INFO:transitions.extensions.nesting:Exited state A
# INFO:transitions.extensions.nesting:Entered state C
# INFO:transitions.extensions.nesting:Entered state C_1
# INFO:transitions.extensions.nesting:Entered state C_2
# INFO:transitions.extensions.nesting:Entered state C_1_a
# INFO:transitions.extensions.nesting:Entered state C_2_z
machine.go()
# INFO:transitions.extensions.nesting:Exited state C_1_a
# INFO:transitions.extensions.nesting:Entered state C_1_b
# INFO:transitions.extensions.nesting:Exited state C_2_z
# INFO:transitions.extensions.nesting:Entered state C_2_x
machine.reset()
# INFO:transitions.extensions.nesting:Exited state C_1_b
# INFO:transitions.extensions.nesting:Exited state C_2_x
# INFO:transitions.extensions.nesting:Exited state C_1
# INFO:transitions.extensions.nesting:Exited state C_2
# INFO:transitions.extensions.nesting:Exited state C
# INFO:transitions.extensions.nesting:Entered state B

当使用parallel而不是children时,transitions将同时进入传递的列表中所有状态。要进入哪个子状态由initial定义,它应该始终指向一个直接子状态。一个新功能是通过在状态定义中传递transitions关键字来定义局部转换。上面定义的转换['go', 'a', 'b']仅在C_1中有效。虽然您可以像在['go', '2_z', '2_x']中那样引用子状态,但您不能在本地定义的转换中直接引用父状态。当父状态退出时,其子状态也将退出。除了从Machine中已知的转换处理顺序(转换按添加的顺序考虑)外,HierarchicalMachine还考虑了层次结构。在子状态中定义的转换将首先评估(例如,在C_1_a离开之前,C_2_z)和使用通配符*定义的转换(目前)只会添加到根状态(在本例中为ABC)中。从0.8.0版本开始,可以直接添加嵌套状态,并将动态创建父状态。

m = HierarchicalMachine(states=['A'], initial='A')
m.add_state('B_1_a')
m.to_B_1()
assert m.is_B(allow_substates=True)

0.9.1中的实验性功能:您可以在状态或HSM本身中使用on_final回调。如果满足以下条件之一,回调将被触发:a) 状态本身被标记为final并且刚刚进入,或b) 所有子状态都被视为final,并且至少有一个子状态刚刚进入final状态。如果b)条件对它们也成立,则所有父状态也将被视为final。这可能在并行处理的情况下很有用,您的HSM或任何父状态应在所有子状态都达到final状态时收到通知。

from transitions.extensions import HierarchicalMachine
from functools import partial

# We initialize this parallel HSM in state A:
#        / X
#       /   / yI
# A -> B - Y - yII [final]
#        \ Z - zI
#            \ zII [final]

def final_event_raised(name):
    print("{} is final!".format(name))


states = ['A', {'name': 'B', 'parallel': [{'name': 'X', 'final': True, 'on_final': partial(final_event_raised, 'X')},
                                          {'name': 'Y', 'transitions': [['final_Y', 'yI', 'yII']],
                                           'initial': 'yI',
                                           'on_final': partial(final_event_raised, 'Y'),
                                           'states':
                                               ['yI', {'name': 'yII', 'final': True}]
                                           },
                                          {'name': 'Z', 'transitions': [['final_Z', 'zI', 'zII']],
                                           'initial': 'zI',
                                           'on_final': partial(final_event_raised, 'Z'),
                                           'states':
                                               ['zI', {'name': 'zII', 'final': True}]
                                           },
                                          ],
                "on_final": partial(final_event_raised, 'B')}]

machine = HierarchicalMachine(states=states, on_final=partial(final_event_raised, 'Machine'), initial='A')
# X will emit a final event right away
machine.to_B()
# >>> X is final!
print(machine.state)
# >>> ['B_X', 'B_Y_yI', 'B_Z_zI']
# Y's substate is final now and will trigger 'on_final' on Y
machine.final_Y()
# >>> Y is final!
print(machine.state)
# >>> ['B_X', 'B_Y_yII', 'B_Z_zI']
# Z's substate becomes final which also makes all children of B final and thus machine itself
machine.final_Z()
# >>> Z is final!
# >>> B is final!
# >>> Machine is final!
重复使用先前创建的HSM

除了语义顺序,嵌套状态在您想要为特定任务指定状态机并计划重用它们时非常有用。在0.8.0版本之前,HierarchicalMachine不会集成机器实例本身,而是通过创建它们的副本来集成状态和转换。然而,从0.8.0版本开始,(Nested)State实例只是被引用,这意味着一个机器的状态和事件的集合中的更改将影响另一个机器实例。不过,模型及其状态不会被共享。注意,事件和转换也是通过引用复制的,如果你没有使用remap关键字,它们将由两个实例共享。这次更改是为了更符合Machine,它也通过引用传递State实例。

count_states = ['1', '2', '3', 'done']
count_trans = [
    ['increase', '1', '2'],
    ['increase', '2', '3'],
    ['decrease', '3', '2'],
    ['decrease', '2', '1'],
    ['done', '3', 'done'],
    ['reset', '*', '1']
]

counter = HierarchicalMachine(states=count_states, transitions=count_trans, initial='1')

counter.increase() # love my counter
states = ['waiting', 'collecting', {'name': 'counting', 'children': counter}]

transitions = [
    ['collect', '*', 'collecting'],
    ['wait', '*', 'waiting'],
    ['count', 'collecting', 'counting']
]

collector = HierarchicalMachine(states=states, transitions=transitions, initial='waiting')
collector.collect()  # collecting
collector.count()  # let's see what we got; counting_1
collector.increase()  # counting_2
collector.increase()  # counting_3
collector.done()  # collector.state == counting_done
collector.wait()  # collector.state == waiting

如果通过children关键字传递了HierarchicalMachine,则此机器的初始状态将被分配给新的父状态。在上面的例子中,进入counting也将进入counting_1。如果这种行为不受欢迎,并且机器应该在父状态中停止,用户可以将initial传递为False,如{'name': 'counting', 'children': counter, 'initial': False}

有时你希望这样的嵌入状态集合“返回”,这意味着在完成之后应该退出并转换到你的超状态之一。为了实现这种行为,你可以重置状态转换。在上面的例子中,我们希望计数器在达到状态done时返回。这是通过以下方式实现的

states = ['waiting', 'collecting', {'name': 'counting', 'children': counter, 'remap': {'done': 'waiting'}}]

... # same as above

collector.increase() # counting_3
collector.done()
collector.state
>>> 'waiting' # be aware that 'counting_done' will be removed from the state machine

如上所述,使用remap复制事件和转换,因为它们可能在原始状态机中无效。如果重用的状态机没有最终状态,你可以当然手动添加转换。如果'counter'没有'done'状态,我们可以简单地添加['done', 'counter_3', 'waiting']来达到相同的行为。

在您想要状态和转换通过值而不是引用复制(例如,如果您想保持0.8之前的旧行为)的情况下,您可以通过创建一个NestedState并将机器的事件和状态的深度副本分配给它来实现这一点。

from transitions.extensions.nesting import NestedState
from copy import deepcopy

# ... configuring and creating counter

counting_state = NestedState(name="counting", initial='1')
counting_state.states = deepcopy(counter.states)
counting_state.events = deepcopy(counter.events)

states = ['waiting', 'collecting', counting_state]

对于复杂的状态机,共享配置而不是实例化的机器可能更可行。特别是由于实例化机器必须从HierarchicalMachine派生。这样的配置可以通过JSON或YAML轻松存储和加载(参见常见问题解答)。HierarchicalMachine允许使用关键字childrenstates定义子状态。如果两者都存在,则仅考虑children

counter_conf = {
    'name': 'counting',
    'states': ['1', '2', '3', 'done'],
    'transitions': [
        ['increase', '1', '2'],
        ['increase', '2', '3'],
        ['decrease', '3', '2'],
        ['decrease', '2', '1'],
        ['done', '3', 'done'],
        ['reset', '*', '1']
    ],
    'initial': '1'
}

collector_conf = {
    'name': 'collector',
    'states': ['waiting', 'collecting', counter_conf],
    'transitions': [
        ['collect', '*', 'collecting'],
        ['wait', '*', 'waiting'],
        ['count', 'collecting', 'counting']
    ],
    'initial': 'waiting'
}

collector = HierarchicalMachine(**collector_conf)
collector.collect()
collector.count()
collector.increase()
assert collector.is_counting_2()

图表

附加关键字

  • title(可选):设置生成图像的标题。
  • show_conditions(默认为False):显示转换边上的条件
  • show_auto_transitions(默认为False):显示图中的自动转换
  • show_state_attributes(默认为False):在图中显示回调(进入、退出)、标签和超时

转换可以生成显示状态之间所有有效转换的基本状态图表。基本图表支持生成mermaid状态机定义,该定义可用于mermaid的实时编辑器、GitLab或GitHub中的markdown文件以及其他网络服务。例如,以下代码

from transitions.extensions.diagrams import HierarchicalGraphMachine
import pyperclip

states = ['A', 'B', {'name': 'C',
                     'final': True,
                     'parallel': [{'name': '1', 'children': ['a', {"name": "b", "final": True}],
                                   'initial': 'a',
                                   'transitions': [['go', 'a', 'b']]},
                                  {'name': '2', 'children': ['a', {"name": "b", "final": True}],
                                   'initial': 'a',
                                   'transitions': [['go', 'a', 'b']]}]}]
transitions = [['reset', 'C', 'A'], ["init", "A", "B"], ["do", "B", "C"]]


m = HierarchicalGraphMachine(states=states, transitions=transitions, initial="A", show_conditions=True,
                             title="Mermaid", graph_engine="mermaid", auto_transitions=False)
m.init()

pyperclip.copy(m.get_graph().draw(None))  # using pyperclip for convenience
print("Graph copied to clipboard!")

生成此图表(请检查文档源以查看markdown表示法)

---
Mermaid Graph
---
stateDiagram-v2
  direction LR
  classDef s_default fill:white,color:black
  classDef s_inactive fill:white,color:black
  classDef s_parallel color:black,fill:white
  classDef s_active color:red,fill:darksalmon
  classDef s_previous color:blue,fill:azure
  
  state "A" as A
  Class A s_previous
  state "B" as B
  Class B s_active
  state "C" as C
  C --> [*]
  Class C s_default
  state C {
    state "1" as C_1
    state C_1 {
      [*] --> C_1_a
      state "a" as C_1_a
      state "b" as C_1_b
      C_1_b --> [*]
    }
    --
    state "2" as C_2
    state C_2 {
      [*] --> C_2_a
      state "a" as C_2_a
      state "b" as C_2_b
      C_2_b --> [*]
    }
  }
  
  C --> A: reset
  A --> B: init
  B --> C: do
  C_1_a --> C_1_b: go
  C_2_a --> C_2_b: go
  [*] --> A

要使用更复杂的图形功能,您需要安装graphviz和/或pygraphviz。要使用graphviz包生成图表,您需要手动或通过包管理器安装Graphviz

sudo apt-get install graphviz graphviz-dev  # Ubuntu and Debian
brew install graphviz  # MacOS
conda install graphviz python-graphviz  # (Ana)conda

现在您可以安装实际的Python包

pip install graphviz pygraphviz  # install graphviz and/or pygraphviz manually...
pip install transitions[diagrams]  # ... or install transitions with 'diagrams' extras which currently depends on pygraphviz

目前,当pygraphviz可用时,GraphMachine将使用pygraphviz,如果找不到pygraphviz,将回退到使用graphviz。如果graphviz也不可用,则将使用mermaid。这可以通过向构造函数传递graph_engine="graphviz"(或"mermaid")来覆盖。请注意,此默认设置可能在将来发生变化,并且可能会放弃对pygraphviz的支持。使用Model.get_graph(),您可以获取当前图或感兴趣区域(roi),并按如下方式绘制:

# import transitions

from transitions.extensions import GraphMachine
m = Model()
# without further arguments pygraphviz will be used
machine = GraphMachine(model=m, ...)
# when you want to use graphviz explicitly
machine = GraphMachine(model=m, graph_engine="graphviz", ...)
# in cases where auto transitions should be visible
machine = GraphMachine(model=m, show_auto_transitions=True, ...)

# draw the whole graph ...
m.get_graph().draw('my_state_diagram.png', prog='dot')
# ... or just the region of interest
# (previous state, active state and all reachable states)
roi = m.get_graph(show_roi=True).draw('my_state_diagram.png', prog='dot')

这将产生类似以下内容:

state diagram example

无论您使用哪种后端,draw函数也接受文件描述符或二进制流作为第一个参数。如果您将此参数设置为None,则将返回字节数据流。

import io

with open('a_graph.png', 'bw') as f:
    # you need to pass the format when you pass objects instead of filenames.
    m.get_graph().draw(f, format="png", prog='dot')

# you can pass a (binary) stream too
b = io.BytesIO()
m.get_graph().draw(b, format="png", prog='dot')

# or just handle the binary string yourself
result = m.get_graph().draw(None, format="png", prog='dot')
assert result == b.getvalue()

作为回调传递的引用和部分将尽可能地得到解决。

from transitions.extensions import GraphMachine
from functools import partial


class Model:

    def clear_state(self, deep=False, force=False):
        print("Clearing state ...")
        return True


model = Model()
machine = GraphMachine(model=model, states=['A', 'B', 'C'],
                       transitions=[
                           {'trigger': 'clear', 'source': 'B', 'dest': 'A', 'conditions': model.clear_state},
                           {'trigger': 'clear', 'source': 'C', 'dest': 'A',
                            'conditions': partial(model.clear_state, False, force=True)},
                       ],
                       initial='A', show_conditions=True)

model.get_graph().draw('my_state_diagram.png', prog='dot')

这将产生类似以下内容:

state diagram references_example

如果引用的格式不符合您的需求,您可以覆盖静态方法GraphMachine.format_references。如果您想完全跳过引用,只需让GraphMachine.format_references返回None。还可以查看我们的示例 IPython/Jupyter笔记本,以获取有关如何使用和编辑图的更详细示例。

线程安全(-ish)状态机

在事件分发在线程中完成的情况下,可以使用LockedMachineLockedHierarchicalMachine,其中function access(注意:这里可能是打字错误)由可重入锁保护。这并不能让您免于通过篡改模型的成员变量或状态机的成员变量来损坏您的机器。

from transitions.extensions import LockedMachine
from threading import Thread
import time

states = ['A', 'B', 'C']
machine = LockedMachine(states=states, initial='A')

# let us assume that entering B will take some time
thread = Thread(target=machine.to_B)
thread.start()
time.sleep(0.01) # thread requires some time to start
machine.to_C() # synchronized access; won't execute before thread is done
# accessing attributes directly
thread = Thread(target=machine.to_B)
thread.start()
machine.new_attrib = 42 # not synchronized! will mess with execution order

可以通过machine_context关键字参数传递任何Python上下文管理器。

from transitions.extensions import LockedMachine
from threading import RLock

states = ['A', 'B', 'C']

lock1 = RLock()
lock2 = RLock()

machine = LockedMachine(states=states, initial='A', machine_context=[lock1, lock2])

通过machine_model传递的任何上下文都将由所有注册了Machine的模型共享。还可以添加每个模型的上下文。

lock3 = RLock()

machine.add_model(model, model_context=lock3)

所有用户提供的上下文管理器必须是可重入的,因为状态机将多次调用它们,甚至在单个触发调用的情况下。

使用异步回调

如果您使用Python 3.7或更高版本,可以使用AsyncMachine来处理异步回调。如果您喜欢,可以混合同步和异步回调,但这可能会产生意想不到的副作用。注意,事件需要等待,并且事件循环也必须由您处理。

from transitions.extensions.asyncio import AsyncMachine
import asyncio
import time


class AsyncModel:

    def prepare_model(self):
        print("I am synchronous.")
        self.start_time = time.time()

    async def before_change(self):
        print("I am asynchronous and will block now for 100 milliseconds.")
        await asyncio.sleep(0.1)
        print("I am done waiting.")

    def sync_before_change(self):
        print("I am synchronous and will block the event loop (what I probably shouldn't)")
        time.sleep(0.1)
        print("I am done waiting synchronously.")

    def after_change(self):
        print(f"I am synchronous again. Execution took {int((time.time() - self.start_time) * 1000)} ms.")


transition = dict(trigger="start", source="Start", dest="Done", prepare="prepare_model",
                  before=["before_change"] * 5 + ["sync_before_change"],
                  after="after_change")  # execute before function in asynchronously 5 times
model = AsyncModel()
machine = AsyncMachine(model, states=["Start", "Done"], transitions=[transition], initial='Start')

asyncio.get_event_loop().run_until_complete(model.start())
# >>> I am synchronous.
#     I am asynchronous and will block now for 100 milliseconds.
#     I am asynchronous and will block now for 100 milliseconds.
#     I am asynchronous and will block now for 100 milliseconds.
#     I am asynchronous and will block now for 100 milliseconds.
#     I am asynchronous and will block now for 100 milliseconds.
#     I am synchronous and will block the event loop (what I probably shouldn't)
#     I am done waiting synchronously.
#     I am done waiting.
#     I am done waiting.
#     I am done waiting.
#     I am done waiting.
#     I am done waiting.
#     I am synchronous again. Execution took 101 ms.
assert model.is_Done()

那么,您可能想知道为什么需要使用Python 3.7或更高版本。异步支持是在更早的时候引入的。AsyncMachine使用contextvars来处理在转换完成之前收到新事件时运行回调。

async def await_never_return():
    await asyncio.sleep(100)
    raise ValueError("That took too long!")

async def fix():
    await m2.fix()

m1 = AsyncMachine(states=['A', 'B', 'C'], initial='A', name="m1")
m2 = AsyncMachine(states=['A', 'B', 'C'], initial='A', name="m2")
m2.add_transition(trigger='go', source='A', dest='B', before=await_never_return)
m2.add_transition(trigger='fix', source='A', dest='C')
m1.add_transition(trigger='go', source='A', dest='B', after='go')
m1.add_transition(trigger='go', source='B', dest='C', after=fix)
asyncio.get_event_loop().run_until_complete(asyncio.gather(m2.go(), m1.go()))

assert m1.state == m2.state

这个示例实际上说明了两个问题:首先,在m1从AB的转换中调用的'go'不会被取消,其次,调用m2.fix()将执行从AC的'fix'来阻止m2从AB的转换尝试。没有contextvars,这种分离是不可能的。注意,prepareconditions不被视为正在进行的转换。这意味着在conditions评估之后,即使已经发生了另一个事件,也会执行转换。只有当作为before回调或更晚时运行的任务才会被取消。

AsyncMachine具有模型特定的队列模式,可以在将queued='model'传递给构造函数时使用。使用模型特定的队列,只有当事件属于同一模型时才会排队。此外,抛出的异常将只清除引发该异常的模型的队列。为了简化,让我们假设以下asyncio.gather中的每个事件都不是同时触发的,而是稍微延迟的。

asyncio.gather(model1.event1(), model1.event2(), model2.event1())
# execution order with AsyncMachine(queued=True)
# model1.event1 -> model1.event2 -> model2.event1
# execution order with AsyncMachine(queued='model')
# (model1.event1, model2.event1) -> model1.event2

asyncio.gather(model1.event1(), model1.error(), model1.event3(), model2.event1(), model2.event2(), model2.event3())
# execution order with AsyncMachine(queued=True)
# model1.event1 -> model1.error
# execution order with AsyncMachine(queued='model')
# (model1.event1, model2.event1) -> (model1.error, model2.event2) -> model2.event3

请注意,在机器构造之后不得更改队列模式。

向状态添加功能

如果您的超级英雄需要一些自定义行为,您可以通过装饰机器状态来添加一些额外功能。

from time import sleep
from transitions import Machine
from transitions.extensions.states import add_state_features, Tags, Timeout


@add_state_features(Tags, Timeout)
class CustomStateMachine(Machine):
    pass


class SocialSuperhero(object):
    def __init__(self):
        self.entourage = 0

    def on_enter_waiting(self):
        self.entourage += 1


states = [{'name': 'preparing', 'tags': ['home', 'busy']},
          {'name': 'waiting', 'timeout': 1, 'on_timeout': 'go'},
          {'name': 'away'}]  # The city needs us!

transitions = [['done', 'preparing', 'waiting'],
               ['join', 'waiting', 'waiting'],  # Entering Waiting again will increase our entourage
               ['go', 'waiting', 'away']]  # Okay, let' move

hero = SocialSuperhero()
machine = CustomStateMachine(model=hero, states=states, transitions=transitions, initial='preparing')
assert hero.state == 'preparing'  # Preparing for the night shift
assert machine.get_state(hero.state).is_busy  # We are at home and busy
hero.done()
assert hero.state == 'waiting'  # Waiting for fellow superheroes to join us
assert hero.entourage == 1  # It's just us so far
sleep(0.7)  # Waiting...
hero.join()  # Weeh, we got company
sleep(0.5)  # Waiting...
hero.join()  # Even more company \o/
sleep(2)  # Waiting...
assert hero.state == 'away'  # Impatient superhero already left the building
assert machine.get_state(hero.state).is_home is False  # Yupp, not at home anymore
assert hero.entourage == 3  # At least he is not alone

目前,转换功能包含以下状态特性:

  • 超时 -- 经过一段时间后触发事件

    • 关键字:timeout (整数,可选) -- 如果传递,进入的状态将在 timeout 秒后超时
    • 关键字:on_timeout (字符串/可调用函数,可选) -- 当超时时间到达时将被调用
    • 当设置 timeout 但未设置 on_timeout 时,将引发 AttributeError
    • 注意:超时在线程中触发。这意味着存在一些限制(例如,捕获超时中引发的异常)。对于更复杂的应用程序,请考虑使用事件队列。
  • 标签 -- 为状态添加标签

    • 关键字:tags (列表,可选) -- 为状态分配标签
    • State.is_<tag_name> 当状态被标记为 tag_name 时将返回 True,否则返回 False
  • 错误 -- 当状态无法离开时引发 MachineError

    • Tags 继承(如果您使用 Error,则不要使用 Tags
    • 关键字:accepted (布尔值,可选) -- 将状态标记为已接受
    • 或者可以传递关键字 tags,包含 'accepted'
    • 注意:只有在将 auto_transitions 设置为 False 时才会引发错误。否则,可以通过 to_<state> 方法退出每个状态。
  • 易失性 -- 每次进入状态时初始化对象

    • 关键字:volatile (类,可选) -- 每次状态进入时,将分配类型为类的对象给模型。属性名称由 hook 定义。如果省略,则创建一个空的 VolatileObject
    • 关键字:hook (字符串,默认='scope') -- 模型中用于临时对象的属性名称。

您可以编写自己的 State 扩展并将它们以相同的方式添加。请注意,add_state_features 期望 混入。这意味着您的扩展应始终调用重写的 __init__enterexit 方法。您的扩展可以继承自 State,但也可以在没有它的情况下工作。使用 @add_state_features 的缺点是装饰机器不能被序列化(更准确地说,动态生成的 CustomState 不能被序列化)。这可能是有必要编写专用自定义状态类的原因。根据选择的状态机,您的自定义状态类可能需要提供某些状态特性。例如,HierarchicalMachine 要求您的自定义状态是 NestedState 的实例(State 不够用)。要注入状态,您可以将它们分配给 Machine 的类属性 state_cls,或者在需要执行某些特定过程时覆盖 Machine.create_state

from transitions import Machine, State

class MyState(State):
    pass

class CustomMachine(Machine):
    # Use MyState as state class
    state_cls = MyState


class VerboseMachine(Machine):

    # `Machine._create_state` is a class method but we can
    # override it to be an instance method
    def _create_state(self, *args, **kwargs):
        print("Creating a new state with machine '{0}'".format(self.name))
        return MyState(*args, **kwargs)

如果您想在 AsyncMachine 中完全避免线程,可以使用 asyncio 扩展中的 AsyncTimeout 替换 Timeout 状态特性

import asyncio
from transitions.extensions.states import add_state_features
from transitions.extensions.asyncio import AsyncTimeout, AsyncMachine

@add_state_features(AsyncTimeout)
class TimeoutMachine(AsyncMachine):
    pass

states = ['A', {'name': 'B', 'timeout': 0.2, 'on_timeout': 'to_C'}, 'C']
m = TimeoutMachine(states=states, initial='A', queued=True)  # see remark below
asyncio.run(asyncio.wait([m.to_B(), asyncio.sleep(0.1)]))
assert m.is_B()  # timeout shouldn't be triggered
asyncio.run(asyncio.wait([m.to_B(), asyncio.sleep(0.3)]))
assert m.is_C()   # now timeout should have been processed

您应该考虑将 queued=True 传递给 TimeoutMachine 构造函数。这将确保事件按顺序处理,并避免超时和事件在接近时出现的异步 竞争条件

与 Django 一起使用 transitions

您可以查看 常见问题解答 以获取一些灵感或查看 django-transitions。它由 Christian Ledermann 开发,并在 Github 上托管。文档 包含一些使用示例

我有 [错误报告/问题/疑问]...

首先,恭喜!您已到达文档的结尾!如果您在安装之前想尝试 transitions,您可以在 mybinder.org 的交互式 Jupyter 笔记本中这样做。只需单击此按钮 👉 Binder

对于错误报告和其他问题,请在GitHub上提交问题

对于使用问题,请在Stack Overflow上发帖,确保将问题标记为pytransitions标签。不要忘记查看扩展示例

对于任何其他问题、请求或大额不限制的金钱礼物,请发邮件给Tal Yarkoni(最初作者)和/或Alexander Neumann(当前维护者)。

项目详情


下载文件

下载适合您平台文件。如果您不确定选择哪个,请了解更多关于安装包的信息。

源分布

transitions-0.9.2.tar.gz (1.2 MB 查看哈希值

上传时间

构建分布

transitions-0.9.2-py2.py3-none-any.whl (111.8 kB 查看哈希值

上传时间 Python 2 Python 3

由以下机构支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面