Hook设计描述
hook,又称钩子,在C/C++中一般叫做回调函数。钩子是从功能角度描述这种编程模式,回调则是从函数调用时间角度描述的。通常理解的hook是在一个已有的方法上加入一些钩子,使得在该方法执行前或执行后另在做一些额外的处理。如我们熟知的windows系统消息响应事件,鼠标点击对程序产生的影响是有程序自己决定的,但是程序的执行是受制于框架(windows系统),框架提供了一些通用的流程执行,但是往往框架或流程在设计时无法完全预料到以后的使用会有什么新需求,或者有些行为只有在运行时才能确定的。这就产生了回调的需求,即用户提供需求,框架负责执行,流程先于具体需求,当触发或者满足某种条件时,执行Hook函数。hook函数的数据也是由用户自己提供的,框架只负责流程执行,这样框架的通用性就能大大提高。
Hook设计三要素
- hook函数或类:实现自定义操作或功能
- 注册:只有经过注册的Hook才能被系统或框架调用
- 挂载点:通常由系统或框架决定,用户无法修改
设计实例
我们看看具体的设计实例:mmcv库的Run类。Run类负责训练流程执行,由用户提供的数据。
Hook类是所有hook类的父类,规定了具体的调用名称和挂载点,如before_run、before_epoch、after_epoch、after_run等,注册的hook类需要具体实现自己的需求,例如实现自定义的学习率更新策略,框架会在Run中每个挂载点循环执行用户注册的所有hooks的相应挂载点方法,用户hooks是放在一个有序列表中,按优先级排列,优先级高的在前,先得到执行,优先级也是由用户确定的,这是用户仅有的权力。
hook调用
形式比较优雅
1 | def call_hook(self, fn_name): |
用户仅仅需要实现自己所需要的hook,如果没有自定义的hook,框架会调用父类Hook中相应的方法。父类Hook可能提供了一些默认行为,也可能什么都没做。
hook实现
Hook设计举例之OptimizerHook,实现了after_train_iter挂载点方法
1 | class OptimizerHook(Hook): |
其实实现hook时,用户的疑问往往是自定义hook需要使用的数据从哪里来?显然用户不知道Run类中有哪些数据。用户其实是知道的,因为Run中原本是没有数据的,它仅是一个流程执行类,其中的数据均来自与用户创建run时传入的,如runner.optimizer。所以可以看到,一个hook仅仅需要两个元素,一个是执行者,这里是runner,另外一个是执行时间(触发条件,挂载点),这里是after_train_iter。
注意一个hook类中具有所有挂载点,但是不必要实现所有挂载点方法,仅需实现本hook需要实现的挂载点方法
hook注册
hook的注册过程比较简单,因为触发是按框架定义的流程顺序主动调用的,因此仅需要按优先级插入到有序列表中即可。
1 | def register_hook(self, hook, priority='NORMAL'): |
如果有一个hook需要在两个不同时机执行两个需求,如在before_train_epoch和after_train_epoch,但是恰巧这两个需求的优先级不同,这个时候建议写成两个hook,每个hook只负责做一件事,这也是编程中一般原则吧。
题外话
如果编写过windows窗体程序(如MFC),hook应该很容易理解,因为在MFC中回调太普遍了,这也是入门难(或者说难深入底层)的原因,框架默默帮你做了很多事,导致新入局者看不到全局。
这种Google风格docstring,我也是比较喜欢的,层次清楚,无多余字符,符合它本来的目的。可能是看惯了,统一就好。
BTW,其实这个知识不应该写在python目录下的,但是如果想到一个知识就按一般化分类,最后可能发现分类太多,知识缺少具体场景依托。因为以后的文章可能位置比较随性~