Python之Hook设计

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
2
3
def call_hook(self, fn_name):
for hook in self._hooks:
getattr(hook, fn_name)(self)

用户仅仅需要实现自己所需要的hook,如果没有自定义的hook,框架会调用父类Hook中相应的方法。父类Hook可能提供了一些默认行为,也可能什么都没做。

hook实现

Hook设计举例之OptimizerHook,实现了after_train_iter挂载点方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class OptimizerHook(Hook):

def __init__(self, grad_clip=None):
self.grad_clip = grad_clip

def clip_grads(self, params):
clip_grad.clip_grad_norm_(
filter(lambda p: p.requires_grad, params), **self.grad_clip)

def after_train_iter(self, runner):
runner.optimizer.zero_grad()
runner.outputs['loss'].backward()
if self.grad_clip is not None:
self.clip_grads(runner.model.parameters())
runner.optimizer.step()

其实实现hook时,用户的疑问往往是自定义hook需要使用的数据从哪里来?显然用户不知道Run类中有哪些数据。用户其实是知道的,因为Run中原本是没有数据的,它仅是一个流程执行类,其中的数据均来自与用户创建run时传入的,如runner.optimizer。所以可以看到,一个hook仅仅需要两个元素,一个是执行者,这里是runner,另外一个是执行时间(触发条件,挂载点),这里是after_train_iter。

注意一个hook类中具有所有挂载点,但是不必要实现所有挂载点方法,仅需实现本hook需要实现的挂载点方法

hook注册

hook的注册过程比较简单,因为触发是按框架定义的流程顺序主动调用的,因此仅需要按优先级插入到有序列表中即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def register_hook(self, hook, priority='NORMAL'):
"""Register a hook into the hook list.

Args:
hook (:obj:`Hook`): The hook to be registered.
priority (int or str or :obj:`Priority`): Hook priority.
Lower value means higher priority.
"""
assert isinstance(hook, Hook)
if hasattr(hook, 'priority'):
raise ValueError('"priority" is a reserved attribute for hooks')
priority = get_priority(priority)
hook.priority = priority
# insert the hook to a sorted list
inserted = False
for i in range(len(self._hooks) - 1, -1, -1):
if priority >= self._hooks[i].priority:
self._hooks.insert(i + 1, hook)
inserted = True
break
if not inserted:
self._hooks.insert(0, hook)

如果有一个hook需要在两个不同时机执行两个需求,如在before_train_epoch和after_train_epoch,但是恰巧这两个需求的优先级不同,这个时候建议写成两个hook,每个hook只负责做一件事,这也是编程中一般原则吧。

题外话

如果编写过windows窗体程序(如MFC),hook应该很容易理解,因为在MFC中回调太普遍了,这也是入门难(或者说难深入底层)的原因,框架默默帮你做了很多事,导致新入局者看不到全局。

这种Google风格docstring,我也是比较喜欢的,层次清楚,无多余字符,符合它本来的目的。可能是看惯了,统一就好。

BTW,其实这个知识不应该写在python目录下的,但是如果想到一个知识就按一般化分类,最后可能发现分类太多,知识缺少具体场景依托。因为以后的文章可能位置比较随性~

参考文献

------ 本文结束------
赞赏此文?求鼓励,求支持!
  • 本文标题: Python之Hook设计
  • 本文作者: Jiang.G.F
  • 创建于: 2019年12月21日 - 23时12分
  • 更新于: 2020年03月03日 - 11时03分
  • 本文链接: https://gfjiangly.github.io/Python/hook.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
0%