写这篇随笔是因为今天自己在写插件和过滤方法的过程中碰壁了,折腾了好久终于稍微发现些问题,在此记下,以作备忘。
在看了xadmin的插件机制后,笔者也想使用该思想来扩展kadmin中视图的方法。
例如,在一个登陆视图中,一般的逻辑如下:
class LoginView(BaseAdminView): '''登陆视图''' auth_form=None#用于认证的表单类 login_template=None title="" def update_login_params(self,defaults): ''' 用于在执行login视图函数前修改参数的钩子''' return defaults @never_cache def get(self,request,*arg,**kwargs): from django.contrib.auth.views import login context=self.get_context(request,*arg,**kwargs)#获取父类的context context.update({ 'title':self.title, 'app_path':request.get_full_path, REDIRECT_FIELD_NAME:request.get_full_path(), }) defaults={ 'extra_context':context, 'authentication_form':self.auth_form or AdminAuthenticationForm, 'template_name':self.login_template or 'kadmin/views/login.html', }
defaults=self.update_login_params(defaults)
return login(request,**defaults) @never_cache def post(self,request,*arg,**kwargs): return self.get(request,*arg,**kwargs)
我们希望能通过update_login_params方法,在执行login函数前更新context,就应该使用插件来接管该方法的执行以按需要进行一些修改。
这里有两种思路:
1、使用过滤器,过滤update_login_params的结果并返回
2、使用插件函数,控制update_login_params的执行(一般是活得update_login_params的执行结果 传入插件函数 由插件函数进行一些修改后再返回)
也可以用插件函数接管update_login_params,并自行决定是否执行update_login_params,或是在update_login_params执行前或后进行一些行为
从上述两点看,插件更为灵活和自由,控制范围更大,二过滤器只能就update_login_params的返回值进行操作并返回。
1、过滤器
定义过滤器装饰器@filter_hook
def filter_hook(func): #过滤器装饰器 #用于对被hook的方法返回值进行再处理 func_name=func.__name__ @wraps(func) def inner_func(self,*arg,**kwargs): if getattr(self,'filters',None): filter_infos=[(getattr(fi,'priority',10),fi) for fi in self.filters if fi.__name__==func_name] filters=[fi for p,fi in sorted(filter_infos,key=lambda x:x[0],reverse=False)] result=func(self,*arg,**kwargs) for fi in filters: result=fi(self,result,*arg,**kwargs) return result else: return func(self,*arg,**kwargs) return inner_func
源码说明:在视图对象self上遍历filters 得到过滤函数,过滤结果并返回结果
例如我们可以定义一个过滤器给say_hello的返回值加上括号
@set_attr(attr_name='__name__',value='say_hello') def add_tag(self,result,o,*arg,**kwargs): return "(%s)"%result
class Person: name='akun' sex="akun-male" plugins=[MalePlugin(sex_num) for sex_num in range(4)] filters=[add_tag] @filter_hook @plugin_hook def say_hello(self,go,*arg,**kwargs): print("-----------done--------") return "Hello i am %s"%self.name if __name__=="__main__": p=Person() print(p.say_hello('go'))
说明:@set_attr是一个装饰器,负责设置函数的属性值,因为filter_hook在查找过滤函数时是找被hook的同名函数,所以需要把add_tag的__name__设为say_hello
2.插件
定义插件装饰器:
def plugin_chain(funcs,token,func,*arg,**kwargs): if token==-1: return func() @wraps(func) def _inner_func(): wrap_func=funcs[token] arg_specs=getargspec(wrap_func)[0] if len(arg_specs)==1: func() return wrap_func(*arg,**kwargs) elif len(arg_specs)>=2 and arg_specs[1]=="__": back=func else: back=func() return wrap_func(back,*arg,**kwargs) return plugin_chain(funcs,token-1,_inner_func,*arg,**kwargs) def plugin_hook(func): func_name=func.__name__ @wraps(func) def method(self,*arg,**kwargs): @wraps(func) def _inner_func(): return func(self,*arg,**kwargs) if getattr(self,'plugins',None): plugin_funcs=[(getattr(p,func_name),getattr(getattr(p,func_name),'priority',10)) for p in self.plugins if getattr(p,func_name,None) ] print(plugin_funcs) #对插件方法按照priority升序排列a plugin_funcs=[p for p,priority in sorted(plugin_funcs,key=lambda x:x[1],reverse=False)] return plugin_chain(plugin_funcs,len(plugin_funcs)-1,_inner_func,*arg,**kwargs) else: return func(self,*arg,**kwargs) return method
源码说明:类似于@filter_hook只是,注意
wrap_func=funcs[token] arg_specs=getargspec(wrap_func)[0] if len(arg_specs)==1: func() return wrap_func(*arg,**kwargs) elif len(arg_specs)>=2 and arg_specs[1]=="__": back=func else: back=func() return wrap_func(back,*arg,**kwargs)
如果若用于接收被hook方法的返回值的参数名为"__"则,__会被设置为被hook的方法,也就是可以通过插件来接管被hook的方法。
我们定义一个插件来在执行say_hello前打印出被hook的方法
class BeforeHook: @set_attr(attr_name='priority',value=6) def say_hello(self,__,o,*arg,**kwargs): print("the back is %s"%__) return __(*arg,**kwargs)
这里的__会被设置为被hook的方法
注意:
正是因为@plugin_hook是需要获取argspecs来得到位置参数的名称,用于判断插件函数是否接受被hook方法的返回值,以及是否在argsepcs[0]中是否
含有双下划线位置参数,如果有则把被hook的方法交给插件函数 而不是把结果交出去。所以基于以上两点,我们不能@filter_hook一个插件方法,因为那样会
包装一个新函数(参数列表为 self,*arg,**kwargs)给plugin_hook,那样的话plugin_hook得到的argsepcs[0]只有self,会认为插件方法不接受返回值,于是
直接调用
if len(arg_specs)==1: func() return wrap_func(*arg,**kwargs)
故会产生错误
但是我们可以在被hook的方法上@filter_hook和@plugin_hook任意组合