State-dependent behaviour of Python objects using Ruby-like eigenclasses with mixins

白昼怎懂夜的黑 提交于 2020-01-14 14:06:09

问题


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...

来源:https://stackoverflow.com/questions/56007866/state-dependent-behaviour-of-python-objects-using-ruby-like-eigenclasses-with-mi

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!