Зависящее от состояния поведение объектов Python, использующих Ruby-подобные собственные классы с миксинами - PullRequest
3 голосов
/ 06 мая 2019

Я искал естественный способ реализации зависимого от состояния поведения (конечных автоматов) в объектах Python. Цель состояла в том, чтобы объекты имели небольшое количество состояний или «взаимно ортогональных» аспектов состояния, которые определяли бы их конкретное поведение в каждый момент. Другими словами, метод, возвращаемый x.foo, должен определяться текущим состоянием x, и если x меняет свое состояние, реализация некоторых или всех его методов должна соответственно изменяться. (Я думаю, что некоторые называют это «государственной схемой проектирования».)

Самое простое решение - хранить методы как атрибуты объекта:

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

Однако сохранение всех методов в качестве атрибутов объекта выглядит как пустая трата памяти, а обновление всех их при каждом изменении состояния выглядит как пустая трата времени / энергии. Кроме того, этот подход не будет работать с именами специальных методов , такими как __str__ или __len__ (если они не настроены для делегирования "обычным" методам).

Наличие отдельного mixin для каждого состояния естественно приходит на ум. Итак, я выяснил, как заставить миксины работать как состояния, используя Ruby-подобные собственные классы вместе с __bases__ мутация 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.

Этот код может быть адаптирован к ситуациям, когда состояние не является "монолитным", но может быть разложено на произведение меньших состояний: Eigenclass должно иметь более одного смешанного "прокси" среди его основ и т. Д.

Был ли описан или протестирован этот или любой другой подобный подход к использованию миксинов в качестве состояний? Есть ли серьезные проблемы с этим? Есть ли "лучшие" альтернативы?


Update.

Я понял важную практическую проблему с использованием мутации __bases__: это должна быть относительно дорогая операция, потому что каждый раз, когда для построения цепочки MRO требуется запуск алгоритма C3 linearisation . Таким образом, изменение баз при каждом изменении состояния очень дорого. Действительно, пытаясь применить этот подход на практике, я наблюдал значительное замедление по сравнению с моим предыдущим решением.

Мне бы хотелось иметь дешевый метод динамического добавления классов в цепочку MRO. Я собираюсь попробовать взломать mro напрямую, используя метакласс ...

...