我一直在寻找一种自然的方式来在 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__
or __len__
(除非它们被设置为委托给“普通”方法)。
很自然地想到每个状态都有一个单独的mixin 。所以我想出了如何使用类似 Ruby 的特征类和突变黑客使mixins作为状态工作:__bases__
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
在其基础中需要有多个mixin“代理”等。
是否已经描述或测试过这种或任何类似的使用 mixins 作为状态的方法?它有什么严重的问题吗?有没有“更好”的选择?
更新。
我已经意识到使用__bases__
突变的一个重要实际问题:它必须是一个相对昂贵的操作,因为每次它都需要运行C3 线性化算法来构建 MRO 链。因此,根据每次状态变化进行修改是非常昂贵的。事实上,尝试在实践中应用这种方法时,与我之前的解决方案相比,我发现速度大幅放缓。
我希望有一种廉价的方法来动态地将类添加到 MRO 链中。我将尝试mro
使用元类直接破解...