ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python 有限状态机库 Automat 完全指南:用 MethodicalMachine 声明式建模状态与序列化

Python 有限状态机库 Automat 完全指南:用 MethodicalMachine 声明式建模状态与序列化 Python 有限状态机库 Automat 完全指南用 MethodicalMachine 声明式建模状态与序列化【免费下载链接】condaA system-level, binary package and environment manager running on all major operating systems and platforms.项目地址: https://gitcode.com/GitHub_Trending/co/conda本篇技术指南以 conda 仓库中随测试环境数据携带的 Automat-0.7.0 包文档DESCRIPTION.rst为核心骨架系统讲解 Automat 这一“为移动中的程序员提供自助式有限状态机”的 Python 库从为什么需要状态机、Automat 的 POPOPlain Old Python Object设计哲学到MethodicalMachine的 input/output/state/upon 四件套 API、返回值的 collector 定制再到状态机的序列化与反序列化。读完你不仅能独立写出可运行的 Automat 状态机代码还能理解 conda 是如何通过 pypi 前缀数据加载器解析这类 wheel 包的dist-info元数据实现 conda 与 pip 安装包互通的。为什么需要状态机以咖啡机为例现实中有大量对象的行为随状态而变化却又希望对外呈现一致的接口。文档用咖啡机做了经典阐述咖啡机有可开合的盖子、水箱、咖啡豆仓和“brew”按钮。它的状态组合很多——有没有水、有没有豆、盖子是否打开、是否正在冲泡。brew按钮只应在特定配置下真正冲泡open lid按钮只有在未冲泡时才能生效。用朴素属性实现时你需要在对象上维护has_water、has_beans、is_lid_open等一系列布尔标志并时刻保证它们彼此一致class CoffeeMachine(object): def brew_button(self): if self.has_water and self.has_beans and not self.is_lid_open: self.heat_the_heating_element() # ...随着咖啡机越来越复杂比如再加一个风味糖浆仓来做榛果咖啡你不得不在每个方法里堆砌越来越多的if检查和状态组合推理。而状态机的价值在于只要你的代码能运行它必然处于所有必需值都已正确初始化的状态因为这些方法必须按照你声明的顺序被调用。用状态机替代零散的if检查把“哪些组合合法”的推理交给机器本身而不是交给散落在各个方法中的布尔判断。Automat 与众不同的地方调用方无需感知状态机PyPI 上实现状态机的库有数十个Automat 的立足点是这个原则用状态机组织代码是好事但调用方不应该、也不必关心你用了状态机。在 Python 中有状态系统的“输入”就是一次方法调用“输出”可以是方法调用触发副作用也可以是返回值内存中的纯计算。多数其他状态机库要求你显式构造一个输入对象、把它交给一个泛化的input方法再从库的接口或自定义类中接收结果。Automat 则完全不同它的输入是普通方法调用输出会自动按upon声明被触发状态只是一些不透明 token。对使用者来说你的类看起来就是一个普通 Python 对象POPO。核心 API 实战MethodicalMachine 的四个装饰器Automat 的核心是MethodicalMachine。首先创建类属性from automat import MethodicalMachine class CoffeeBrewer(object): _machine MethodicalMachine()然后按职责切分逻辑。输入input只声明接口不写实现——输入方法被调用时其实不会执行函数体真正的“干活”是输出output的职责_machine.input() def brew_button(self): The user pressed the brew button. _machine.output() def _heat_the_heating_element(self): Heat up the heating element, which should cause coffee to happen. self._heating_element.turn_on()接着声明两个状态state。为简单起见这里只有have_beans和dont_have_beans两个状态dont_have_beans被标记为initialTrue因为CoffeeBrewer初始时没有豆子_machine.state() def have_beans(self): In this state, you have some beans. _machine.state(initialTrue) def dont_have_beans(self): In this state, you dont have any beans.再补一个输入put_in_beans_machine.input() def put_in_beans(self): The user put in some beans.最后通过状态装饰出的函数自带的upon方法把所有东西串联起来——upon声明“在某个状态下遇到某个输入时进入什么状态、触发哪些输出”# 没有豆子时放入豆子 → 进入 have_beans 状态不产生任何输出 dont_have_beans.upon(put_in_beans, enterhave_beans, outputs[]) # 有豆子时按下 brew 按钮 → 进入 dont_have_beans 状态 # 输出为加热加热元件 have_beans.upon(brew_button, enterdont_have_beans, outputs[_heat_the_heating_element])对使用CoffeeBrewer的人来说它依然是个普通对象 coffee_machine CoffeeMachine() coffee_machine.put_in_beans() coffee_machine.brew_button()所有输入都像方法一样直接调用所有输出都按upon中声明的顺序自动触发所有状态只是不透明 token——不过由于状态和输入输出一样是用方法定义的你可以在其上写 docstring 方便文档化。不要查询状态把状态判断封装进机器内部Automat 最重要的设计忠告是不要获取状态机的当前状态。状态机的价值就在于让调用方在恰当的时间提供恰当的输入而不用自己检查状态。如果你忍不住写出if connection_state_machine.state CONNECTED: connection_state_machine.send_message() else: print(not connected)正确做法是让调用方直接调用connection_state_machine.send_message()然后把“是否已连接”的判断收进状态机内部_machine.state() def connected(self): connected _machine.state() def not_connected(self): not connected _machine.input() def send_message(self): send a message _machine.output() def _actually_send_message(self): self._transport.send(bmessage) _machine.output() def _report_sending_failure(self): print(not connected) connected.upon(send_message, enterconnected, [_actually_send_message]) not_connected.upon(send_message, enternot_connected, [_report_sending_failure])这样“当前处于什么状态”的责任始终保留在状态机内部调用方只需关心“我要发消息”这一意图。给输入传参、给输出取返回值collector 的用法状态机的方法同样支持参数与返回值。比如把豆子放进机器时你可能想保存“这是什么豆子”。在 Automat 中输入方法不能添加任何实现代码——输入纯粹是接口声明行为必须全部来自输出因此状态变更要表达为输出_machine.input() def put_in_beans(self, beans): The user put in some beans. _machine.output() def _save_beans(self, beans): The beans are now in the machine; save them. self._beans beans dont_have_beans.upon(put_in_beans, enterhave_beans, outputs[_save_beans])调用coffee_machine.put_in_beans(real good beans)后机器会记住这些豆子。取回结果则需要让某个输出带返回值。给冲泡流程加一个描述咖啡的输出并把_describe_coffee挂到冲泡转换上_machine.output() def _describe_coffee(self): return A cup of coffee made with {}..format(self._beans) have_beans.upon(brew_button, enterdont_have_beans, outputs[_heat_the_heating_element, _describe_coffee])注意_describe_coffee里不需要先检查self._beans是否存在因为只有状态机确认已经走过会设置该属性的状态序列这个输出方法才可能被到达。调用结果却是 coffee_machine.brew_button() [None, A cup of coffee made with real good beans.]那个None是怎么回事由于每个输入可以产生多个输出Automat 中每次输入调用的默认返回值是一个 list此处是加热元件和描述咖啡两个输出的返回值。这可以用upon的collector参数定制collector是一个可调用对象接收所有输出返回值的可迭代对象并把它们“收集”成单个返回值返回给状态机的调用者。如果只关心最后一个输出have_beans.upon(brew_button, enterdont_have_beans, outputs[_heat_the_heating_element, _describe_coffee], collectorlambda iterable: list(iterable)[-1] )现在就能得到期望的单个返回值 coffee_machine.brew_button() A cup of coffee made with real good beans.collector的另一个典型用法是next在 LightSwitch 示例 中on_state.upon(query_power, enteron_state, outputs[_is_powered], collectornext)直接从输出返回值序列中取第一个元素从而让query_power()直接返回True/False。保存与恢复状态机序列化与反序列化虽然不能直接查询状态但状态机提供了序列化 API可以把状态保存到数据库、API 响应、磁盘文件等。首先通过MethodicalMachine.state()装饰器的serialized参数为每个状态选定一个持久化表示。以下是一个只有开/关两种状态、翻转即反转的“电灯开关”状态机class LightSwitch(object): _machine MethodicalMachine() _machine.state(serializedon) def on_state(self): the switch is on _machine.state(serializedoff, initialTrue) def off_state(self): the switch is off _machine.input() def flip(self): flip the switch on_state.upon(flip, enteroff_state, outputs[]) off_state.upon(flip, enteron_state, outputs[])这里“开”状态用字符串on表示“关”状态用off表示。再加一个查询通电状态的输入和两个返回布尔值的输出_machine.input() def query_power(self): return True if powered, False otherwise _machine.output() def _is_powered(self): return True _machine.output() def _not_powered(self): return False on_state.upon(query_power, enteron_state, outputs[_is_powered], collectornext) off_state.upon(query_power, enteroff_state, outputs[_not_powered], collectornext)保存状态使用MethodicalMachine.serializer()。被serializer()装饰的方法会在参数列表开头被注入一个额外参数——当前状态的序列化标识此处为on或off。由于状态机的输出方法也可能影响对象上的其他状态serializer 方法被期望返回序列化所需的全部相关状态_machine.serializer() def save(self, state): return {is-it-on: state}Serializer 可以是公开方法返回内容完全由你决定。如果必要可以定义多个_machine.serializer()方法分别对应不同格式——一个返回 JSON 用的数据结构、一个返回 XML、一个返回数据库行等等。反序列化则通常用私有方法因为 unserializer 需要接收一个尚未完全初始化的实例并为其填充状态。它被期望返回传给 serializer 的那个序列化状态 token但参数可以任意通常它会以配对 serializer 的返回值为参数_machine.unserializer() def _restore(self, blob): return blob[is-it-on]实践中一般再手写一个类方法构造器来调用它从而知道如何创建你的对象实例classmethod def from_blob(cls, blob): self cls() self._restore(blob) return self保存与加载LightSwitch连同其状态机状态的完整流程 switch1 LightSwitch() switch1.query_power() False switch1.flip() [] switch1.query_power() True blob switch1.save() switch2 LightSwitch.from_blob(blob) switch2.query_power() True一个来自 Automat 项目docs/examples目录的更完整经过测试、可运行示例可供进一步参考。总之去把所有的状态都“机器化”吧Go forth and machine all the state!。仓库语境这份文档为何出现在 conda 中你阅读的这份 DESCRIPTION.rst 并不是 conda 仓库自有的技术文档而是 conda 测试框架内置的真实环境快照数据的一部分。它位于 tests/data/env_metadata/py36-osx-whl 下模拟一个用 pip 装过 Automat-0.7.0 wheel 的 Python 3.6 macOS 环境。Automat-0.7.0 的dist-info目录里存放了标准的 wheel 元数据METADATA含Requires-Dist: attrs (16.1.0)、Requires-Dist: six及visualizeextra 依赖、RECORD逐文件列出 sha256 与字节数、metadata.json结构化元数据声明了automat-visualize这个 console_scripts 入口automat._visualize:tool、top_level.txt和entry_points.txt等见 RECORD。这些快照被test_pip_interop等测试消费tests/core/test_prefix_data.py测试以interoperabilityTrue创建PrefixData并调用load_site_packages断言自动扫描出的 pip 安装包集合——在envpy27win_whl与envpy37osx_whl两个环境里都明确包含automat。其底层实现是 conda/plugins/prefix_data_loaders/pypi/init.py 中的load_site_packages通过get_site_packages_anchor_files扫描 site-packages 中的*.dist-info/RECORD、*.egg-info/PKG-INFO等锚点文件把不属于 conda 管理的 Python 包转换为内存中的前缀记录而 conda/plugins/prefix_data_loaders/pypi/pkg_format.py 则负责解析RECORD、METADATA、entry_points.txt等格式如PythonInstalledDistribution校验RECORD/METADATA/INSTALLER必须存在并支持通过get_conda_dependencies把Requires-Dist与环境标记转换为 conda 依赖约束见 tests/common/pkg_formats/test_python.py 中的相关用例。也就是说Automat 的这份状态机教程在 conda 仓库中扮演着双重角色——对 conda 开发者而言它是验证 pip/conda 互通PyPI 元数据解析能力的标准测试样本对读者而言它本身就是一份完整、可运行的状态机编程指南。结语Automat 用“方法调用即输入、自动触发即输出、不透明 token 即状态”的设计把状态机的表达成本降到最低MethodicalMachine上的input()、output()、state()与upon()四个原语即可完成全部建模collector让你精确控制每次调用的返回值而serialized、serializer()与unserializer()则让状态持久化变得直白。把状态判断封装进机器内部、让调用方只表达意图是使用 Automat 时最值得牢记的设计原则——这也是它在众多 Python 状态机库中保持简洁与地道的根本原因。【免费下载链接】condaA system-level, binary package and environment manager running on all major operating systems and platforms.项目地址: https://gitcode.com/GitHub_Trending/co/conda创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表