参数解析器 (Dispatcher) 及其接口
note
为了后续叙述的方便, 我们先在这里约定好某些术语的中文翻译, 我们认为这些翻译非常准确, 但也请各位见仁见智.
dispatch
: vt & vi. 解析(参数);dispatcher
: n. 参数解析器;Dispatcher Interface
: 参数解析器接口, 缩写为DiI
或dii
.
我们在前面已经叙述过了我们创造 Broadcast Control
的动机,
而我们将在这一章中说明在 Broadcast Control
中执行事件监听器的流程, 具体细节, 以及它的至今为止和从今往后.
至今为止的故事
在很久之前, 有一库, 名曰 python-mirai
. 这个库实际就是 Graia Framework
的前身,
并且其事件监听的语法和现在的 Graia Framework
非常像, 内部处理也是通过对函数形式参数的定义进行解析,
以使语法尽可能简洁...?
哦上帝, 这孩子解析参数的实现方式居然只是通过一个字典, 并且是内联的, 与事件的定义强耦合, 使得每增加一个注解解释就极其可能崩掉整个应用, 开发成本随复杂度直线上升...
在经过几个月的沉思与停更, 我们设计出了 Broadcast Control
.
现今
我们设计出了 Dispatcher
和 DispatcherInterface
.
note
我们之所以一次提到两个事物, 是因为这两样之间的联系实在是太紧密, 源自于如果分开讲, 内容可能连我们都会不知所云...这可不好.
我们主要在 Broadcast.Executor
这个方法处完成了对函数调用, 或者说, 对 Callable
对象的 Call
操作的高层封装.
执行 Broadcast.Executor
一次的操作就叫做 "执行"(Execute
), 这个方法也就根据字面意思译作 "执行器".
到现在为止还挺好理解, 不是么?
处理 Dispatcher 集合 / 列表
在定义事件时, 我们会要求事件内定义一个 Dispatcher
的类, 并且继承自 BaseDispatcher
.
这个类用于定义事件本身的 Dispatcher
.
我们在设计 Dispatcher
时, 引入了 "混入"(mixin
) 的设计:
假设我们有一个叫做 TestDispatcher_1
的 Dispatcher
:
并且有 TestDispatcher_2
, TestDispatcher_3
, 内容暂时不用关心,
我们只要知道:
那么, 我们对于 mixin
的处理函数 dispatcher_mixin_handler
的行为是这样的:
顺带一提...
如果混入链中包含有重复的项, 也不会对其进行去重处理.
然后我们会对上一步得出的结果进行进一步的处理, 这个过程我们暂时称为 "预注入".
这个处理流程其实就是将"环境"中的 Dispatcher
给加进去, 这里的 "环境" 包含以下引用:
- 如果执行时, 传入的
target
类型为Listener
,Listener.inline_dispatchers
和Listener.namespace.injected_dispatchers
会被依次插入到之前结果的头部. - 如果执行时, 传入了
dispatcher
参数且参数值合法, 也会在上一项操作执行后被插入.
总结下来, "预注入" 的优先级是这样, 最上面的是最先被注入的, 这意味着最下面的拥有最高处理优先级:
Listener.inline_dispatchers
Listener.namespace.injected_dispatchers
- 传入的参数
dispatcher
用代码简单的表示:
哦, 我们还没有提到过, 真正可以用的 Dispatcher
的类型注解:
这意味着 dispatcher
可以用来称呼:
- 一个继承了
BaseDispatcher
的类; - 一个继承了
BaseDispatcher
的类的实例; - 一个接受一个类型为
DispatcherInterface
的任意Callable
.
note
值得注意的是...我们并没有在实际的代码中实装这一类型注解, 这里仅仅只是拿来辅助说明而已.
接触 Dispatcher Interface
DispatcherInterface
, 即 "参数解析器接口", 在 Broadcast Control
中负责从一个既定的 Dispatcher
集合中获取与当前上下文相匹配的值.
这个值则由 Dispatcher
的方法 catch
或者它本身返回, 后者仅在当目标 Dispatcher
为一 Callable
时发生.
那么, "上下文"(Context
) 从何而来?
warning
这里的上下文与 Graia Application for Mirai
中所包含的模块 graia.application.context
无关.
我们使用 Python 标准库 inspect
扫描所给出函数的参数定义, 主要获取以下几个信息:
name
, 即参数的名称;annotation
, 与参数名称相对应的类型注解(Type Annotation
);default
, 参数的默认值.
于是我们使用 Python 的 with statement
特性, 调用了 DispatcherInterface.enter_context
方法;
note
如果你对 DispatcherInterface
实例的位置感兴趣...它现在随 Broadcast
对象共享同一个生命周期,
你可以通过获取 Broadcast.dispatcher_interface
获取它.
这个方法向 DispatcherInterface
内部维护的上下文栈推送一个 "执行上下文",
该上下文环境包括两个东西:
- 当前正在处理的事件实例;
- 之前处理得到的
Dispatcher
集合.
之后的一切才真正开始.
窥探 Dispatcher Interface
我们在上一节中讲到, 我们使用 inspect
解析函数的参数定义,
实际代码中完成这项工作的是 argument_signature
函数, 它返回一个 Tuple[str, Any, Any]
,
正好与 name, annotation, default
的顺序相符合.
之后, 我们使用了 DispatcherInterface.execute_with
方法.
note
在这里我们贴上 DispatcherInterface.execute_with
的定义:
看起来蛮奇怪的, 不是么?
事实上, 这是真的.
DispatcherInterface.execute_with
负责与 Dispatcher
集合中的 Dispatcher
交互,
Broadcast.Executor
则将该方法的返回值作为参数传入. 返回值的类型注解是 Any
,
因为我们无法推断 Dispatcher
的返回值的类型, 毕竟它甚至能抛出异常(RequirementCrashed
).
那么, DispatcherInterface.execute_with
又是如何与 Dispatcher
交互的?
当 DispatcherInterface.execute_with
执行时, 它会先向内部的 "参数解析上下文栈" 推送新的上下文实例,
这个上下文实例包含了方法被执行时传入的所有参数和一些其他的东西.
note
如果你对这个上下文实例感兴趣...它叫 ExecuteContextStackItem
,
在 graia.broadcast.interfaces.dispatcher
处被定义,
与 DispatcherInterface
的定义在一个模块下面.
别被文字误导了, 这只是个笔误, 我们仍然用 "参数解析上下文" 称呼它.
tip
你可能会迷惑: 这么多的 "上下文", 设计出来做什么?
你会知道这个设计所带来的惊人的可扩展性的, 不过...我们在这里只讲它的工作流程, 可能性则由你们去发掘了.
上下文推进去了, 接下来是一系列迷离的东西, 我们会分开来讲述的, 这里我们先将这些内容分为几大块:
- 普通的参数解析流程;
alive_generator_dispatcher
相关(与第一部分有重合);- "Always Dispatcher" 相关.
普通的参数解析
在这种用途中, Dispatcher
真的就是拿来解析参数的, 猜到了吧,
这玩意究竟被用作什么不用我说了.
在 DispatcherInterface.execute_with
中, 一系列的奇怪的处理后, 就要开始访问 Dispatcher
了, 大概的步骤是这样的:
- 遍历先前提供的执行上下文中包含有的
Dispatcher
集合, 这里我们将遍历过程中得到的单一值称为current_dispatcher
, 即 "当前参数解析器"; - 分析调用的方式, 并将得到的
Callable
存储到对象dispatcher_callable
:- 如果是一个继承自
BaseDispatcher
的类: 实例化, 且实例化时不带任何参数, 之后获取其方法catch
作为接下来被调用的对象; - 如果是一个继承自
BaseDispatcher
的类实例: 获取其方法catch
作为接下来被调用的对象; - 如果只是
Callable
:dispatcher_callable = current_dispatcher
; - 什么也不是: 抛出
ValueError
.
- 如果是一个继承自
- 判断
dispatcher_callable
是否是(异步)生成器函数:- 如果是: 则调用并生成一个值, 作为参数解析的结果
result
; - 如果不是: 调用并捕获返回值, 作为参数解析的结果
result
.
- 如果是: 则调用并生成一个值, 作为参数解析的结果
- 判断
result
:- 如果是
None
: 继续遍历, 即 "继续向下查询"; - 如果是特殊对象容器
Force
的实例: 获取其属性content
的值并返回, 作为参数解析的结果; - 如果以上条件都不满足: 将
result
返回, 作为参数解析的结果.
- 如果是
如果本次执行过程中有生成器被调用, 则会在当前执行完毕后, 即 DispatcherInterface.enter_context
方法的 with statement
代码块执行完后,
尝试生成最多 15 个值, 与之前的一个加起来, 总共一个生成器最多被调用 16 次,
如果生成完 15 个值后还没有停下来的意思, 则会抛出一 OutOfMaxGenerater
(超过最大生成量) 错误.
tip
如果你对生成器作为 Dispatcher
并参与到参数的解析过程这个步骤感到迷惑, 你可以这样理解:
也可能不参与, 毕竟第一个生成的值如果是
None
的话也会触发 "继续向下查询" 的行为
alive_generator_dispatcher 相关
我们在上一节中谈过了生成器的部分, 这里仅仅是分出来便于理解.
生成器也可以作为 Dispatcher
被 DispatcherInterface.execute_with
访问,
其行为正如上所述:
如果本次执行过程中有生成器被调用, 则会在当前执行完毕后, 即
DispatcherInterface.enter_context
方法的with statement
代码块执行完后, 尝试生成最多 15 个值, 与之前的一个加起来, 总共一个生成器最多被调用 16 次, 如果生成完 15 个值后还没有停下来的意思, 则会抛出一OutOfMaxGenerater
(超过最大生成量) 错误.
Always Dispatcher
我们将属性 always
的值为 True
的继承自 BaseDispatcher
的类实例称为 "Always Dispatcher",
此种 Dispatcher
在一次 DispatcherInterface.execute_with
方法执行流程中至少会被执行/访问一次,
执行次数 execute_count
的值域为 [1, +∞)
.
结束...?
似乎并没有.
- 在参数解析的过程中, 有可能因为用户配置不当, 导致出现
RequirementCrashed
这个错误: 这个错误是因为现有的Dispatcher
集合中的任何一个Dispatcher
都无法处理用户所定义的参数; - 当
Dispatcher
被调用时, 你要记住, 它可以访问到整个DispatcherInterface
: 我们并没有对Dispatcher
对后者的修改和使用, 也就是说, 它也可以使用execute_with
方法, 从自它以后的Dispatcher
集合中获取值, 并可以对其进行包装和修改, 判断, 一系列的操作都可以.
tip
在我们所提供的 Depend
中就使用了这个特性, 效果非常的好.
此外, 无论是 Dispatcher
还是之后会讲到的 Decorator
,
当其抛出错误时, 都会终止本次执行, 并广播 ExceptionThrowed
事件;
我们不保证在监听了 ExceptionThrowed
事件的函数如果抛出错误会不会导致整个系统崩溃,
但大概率, 嗯, 有的, 所以这玩意还是挺危险的.
你可以去看下一章了, 如果有的话, 我们会谈谈 Decorator
.