I've been looking for a natural way to implement state-dependent behaviour (state machines) in Python objects. The goal was for objects to have a small number of states, or "mutually orthogonal" aspects of state, which would determine their concrete behaviour at each moment. In other words, the method returned by x.foo
should be determined by the current state of x
, and if x
changes its state, the implementation of some or all of its methods should change accordingly. (I think some call it "state design pattern.")
The most straightforward solution is to store methods as object attributes:
class StatefulThing:
def _a_say_hello(self):
print("Hello!")
self.say_hello = self._b_say_hello
self.say_goodbye = self._b_say_goodbye
return True
def _a_say_goodbye(self):
print("Another goodbye?")
return False
def _b_say_hello(self):
print("Another hello?")
return False
def _b_say_goodbye(self):
print("Goodbye!")
self.say_hello = self._a_say_hello
self.say_goodbye = self._a_say_goodbye
return True
def _init_say_goodbye(self):
print("Why?")
return False
def __init__(self):
self.say_hello = self._a_say_hello
self.say_goodbye = self._init_say_goodbye
However, storing all methods as object attributes looks like a waste of memory, and updating all of them on every change of state looks like a waste of time/energy. Also, this approach will not work with special method names like __str__
or __len__
(unless they are set up to delegate to "ordinary" methods).
Having a separate mixin for each state comes naturally to mind. So I've figured out how to make mixins work as states using Ruby-like eigenclasses together with __bases__
mutation hack:
class T:
"""
Descendant of `object` that rectifies `__new__` overriding.
This class is intended to be listed as the last base class (just
before the implicit `object`). It is a part of a workaround for
* https://bugs.python.org/issue36827
"""
@staticmethod
def __new__(cls, *_args, **_kwargs):
return object.__new__(cls)
class Stateful:
"""
Abstract base class (or mixin) for "stateful" classes.
Subclasses must implement `InitState` mixin.
"""
@staticmethod
def __new__(cls, *args, **kwargs):
# XXX: see https://stackoverflow.com/a/9639512
class CurrentStateProxy(cls.InitState):
@staticmethod
def _set_state(state_cls=cls.InitState):
__class__.__bases__ = (state_cls,)
class Eigenclass(CurrentStateProxy, cls):
__new__ = None # just in case
return super(__class__, cls).__new__(Eigenclass, *args, **kwargs)
# XXX: see https://bugs.python.org/issue36827 for the reason for `T`.
class StatefulThing(Stateful, T):
class StateA:
"""First state mixin."""
def say_hello(self):
self._say("Hello!")
self.hello_count += 1
self._set_state(self.StateB)
return True
def say_goodbye(self):
self._say("Another goodbye?")
return False
class StateB:
"""Second state mixin."""
def say_hello(self):
self._say("Another hello?")
return False
def say_goodbye(self):
self._say("Goodbye!")
self.goodbye_count += 1
self._set_state(self.StateA)
return True
# This one is required by `Stateful`.
class InitState(StateA):
"""Third state mixin -- the initial state."""
def say_goodbye(self):
self._say("Why?")
return False
def __init__(self, name):
self.name = name
self.hello_count = self.goodbye_count = 0
def _say(self, message):
print("{}: {}".format(self.name, message))
def say_hello_followed_by_goodbye(self):
self.say_hello() and self.say_goodbye()
# ----------
# ## Demo ##
# ----------
if __name__ == "__main__":
t1 = StatefulThing("t1")
t2 = StatefulThing("t2")
print("> t1, say hello.")
t1.say_hello()
print("> t2, say goodbye.")
t2.say_goodbye()
print("> t2, say hello.")
t2.say_hello()
print("> t1, say hello.")
t1.say_hello()
print("> t1, say hello followed by goodbye.")
t1.say_hello_followed_by_goodbye()
print("> t2, say goodbye.")
t2.say_goodbye()
print("> t2, say hello followed by goodbye.")
t2.say_hello_followed_by_goodbye()
print("> t1, say goodbye.")
t1.say_goodbye()
print("> t2, say hello.")
t2.say_hello()
print("---")
print( "t1 said {} hellos and {} goodbyes."
.format(t1.hello_count, t1.goodbye_count) )
print( "t2 said {} hellos and {} goodbyes."
.format(t2.hello_count, t2.goodbye_count) )
# Expected output:
#
# > t1, say hello.
# t1: Hello!
# > t2, say goodbye.
# t2: Why?
# > t2, say hello.
# t2: Hello!
# > t1, say hello.
# t1: Another hello?
# > t1, say hello followed by goodbye.
# t1: Another hello?
# > t2, say goodbye.
# t2: Goodbye!
# > t2, say hello followed by goodbye.
# t2: Hello!
# t2: Goodbye!
# > t1, say goodbye.
# t1: Goodbye!
# > t2, say hello.
# t2: Hello!
# ---
# t1 said 1 hellos and 1 goodbyes.
# t2 said 3 hellos and 2 goodbyes.
This code can be adapted to situations where the state is not "monolithic" but can be decomposed into a product of smaller states: Eigenclass
would need to have more than one mixin "proxy" among its bases, etc.
Has this or any similar approach of using mixins as states been described or tested? Are there any serious problems with it? Are there "better" alternatives?
Update.
I have realised an important practical issue with using __bases__
mutation: it must be a relatively expensive operation because each time it requires running a C3 linearisation algorithm to construct the MRO chain. Modifying bases on each change of state is thus very expensive. Indeed, trying to apply this approach in practice I observed a big slowdown compared to my previous solution.
I would have liked to have a cheap method for dynamically prepending classes to the MRO chain. I am going to try to hack mro
directly using a metaclass...
foo
,MyClass
, ordoSomething()
) leaves too much to the imagination." In "What types of questions should I avoid asking?": "Best practices in general", "Higher-level architecture and design of software systems". – Crossroad__class__
of the object on each state change? Python is not really meant to support a class per object (because of extensive caching of type information to speed up operations on the objects of that type). – Cointon