问题背景
大约在上个月,有这样一个需求,需要为HPC的用户提供一个统一的任务提交脚本。用户可以使用这个脚本提交任务计算。我们实际使用的任务调度系统是PBSpro。因此这个主要的功能是:
- 为各种不同的计算软件提供一个一致的入口,对复杂的pbs提交参数进行检测
- 在任务提交前对用户参数进行校验和检查
- 在任务提交前根据当前队列的状态,优化提交参数
这是一个简单的需求。我个人比较喜欢argparse这个库。能够很容易实现各种模式的命令行参数。当然也有一些同学喜欢click, Fire或者是optparse,今天就不多做讨论了。
最初的版本大概是这样子的
pbs_sub.py
|
|
整个脚本的大致结构为:
- 库和全部变量
- 一些公共的辅助函数,以及某些软件所使用的函数
- 公共的命令行解析器
- 所有软件的命令行解析器作为公共命令行解析器的子解析器
- 公共的命令行处理过程
- 用if-else放置不同软件的处理函数
这个版本执行起来是没有问题的。但是依然会有几个问题:
- 如果需要支持的软件很多,那么整个代码会变得非常长
- 这个脚本需要经常改动。某个软件改动如果搞错可能会影响到不相干的软件执行
为了解决这个问题,有两个实现方案:
- 实现一个公共库,把公共解析器以及公共函数放在里面,每个软件用一个单独的入口脚本,通过引用公共库来实现代码复用
- 实现一个公共库,把公共解析器以及公共函数放在里面。再将各个软件当作插件装载进来。这个方案需要解决的问题打乱插件的代码顺序,先执行所有parser的注册,最后根据用户输入参数的解析,将请求路由到恰当的插件里
今天要讲的,就是第二种方案的一种实现
__init_subclass__魔术方法介绍
在这个方案中,使用了__initsubclass__这个实现子类的注册和回调。因此我们首先需要了解一下\_init_subclass__的作用和使用方法。
__init_subclass__是python3.6引入的一个新的魔术方法。使用这个魔术方法,可以在子类定义的过程中作为钩子被调用,作用机制和元类相似。最大的区别就是这个魔术方法只在子类构建的时候被调用,父类是不会调用的。实际用起来更为简洁,编程体验也更好。元类说到底,还是有些麻烦的。而且更为重要的是,这个魔术方法可以和元类一起使用,不会产生冲突。经常使用元类编程的同学遇到的问题可能就是无法同时使用两种元类,有了这个魔术方法,代码设计上又增加了很大的灵活性。
现在我们举个具体的例子来说明__init_subclass__是如何使用的。
|
|
我们先定义一个元类,以及一个使用该元类的类A,输出结果为
|
|
从输出的结果可以看到,在定义类A的时候,MetaData被触发
|
|
我们用A作为父类创建一个子类B,输出结果为:
|
|
从输出的结果来看,调用的顺序是先调用__init_subclass__,再调用元类。
我们再创建一个实例看看:
|
|
输出结果为:
|
|
这个表现符合预期,没什么多说的。我们再用B为父类创建一个孙子类来看看__init_subclass__是否会执行
|
|
输出结果为
|
|
可以看到,执行结果和B一样,说明__init_subclass__是可以传递的。我们再来试试方法重写
|
|
|
|
|
|
执行结果为:
|
|
可以看到,__init_subclass__可以像常规的的方法一样实现重写。我们再来试试多重继承:
|
|
|
|
执行结果为:
|
|
E中的__init_subclass__并没有执行,但是并没有有冲突错误。
|
|
执行结果为
|
|
因为使用了supper(),A中的__init_subclass__得到了执行。看起来,多重继承的表现和普通的方法没什么两样。
经过一系列实验,最后得到的结论是在同时使用metaclass、__initsubclass__, \_new与__init的时候。在类的构建期依次调用__initsubclass__,与metaclass,而在实例构建的时候,依次调用\_new与__init。
而__init_subclass__的特性,和普通的方法区别不大。如果想有更多的了解,可以进一步参考PEP 487
利用__init_subclass__实现注册和回调机制
说完了__init_subclass__的用法以后,我们再具体看一下在这个实际案例中,如何利用这个魔术方法的特性,实现代码的解藕。
先上代码:
pbs_sub.py
|
|
software/__init__.py
|
|
software/mainparser.py
|
|
software/powerflow.py
因为涉及到具体的业务逻辑,因此整个实现就被我省略了,这里只是为了演示整个结构
|
|
从整个代码结构上, 我们可以看到。
公共的逻辑被放置到了MainParser中,而powerflow的业务逻辑,则被独立开来,放置到了一个单独的文件内。并通过类的继承,复用了公用的类的方法,变量,以及数据缓存。
整个软件的逻辑如下:
- 在MainParser类中,通过__init_subclass__,在类还在创建过程中,就将整个类对象放置到自己的全局字典中。而类创建代码,则是在import的时候执行
- 在MainPasrer进行实例初始化的时候,依次调用所有子类的add_parser方法,实现所有插件parser的注册
- 最后在实例化的mainparser对象中,通过run方法,解析用户的输入,并路由给对应的插件handle处理方法。拿到对应handle的处理后,在进行下一道工序的处理
所有的MainParser的类中,所有的子类均没有进行实例化,因为并没有必要。唯一实例化的是MainParser本身。
而当你需要添加一个新的计算软件。那么你只需要:
在software中放置一个单独的文件,写一个MainParser的子类.
在这个子类中,需要实现:
- 定义__software,以及__version类变量。注册插件信息
- 实现add_parser类方法,提供子命令的入口给mainparser回调
- 实现handle类方法,提供给mainparser回调
- 最后通过修改sotware/__init__.py将新的代码引入即可
如果觉得每次都要import一下新的代码很烦,可以定制import的过程做自动加载,但是目前来看,并没有多少必要来做这个事,还是从简的好。
总结
通过使用python元编程,我们往往可以使用极少的代码就可以实现比较复杂的设计模式。这也python是让人非常上瘾的一点。