使用Python装饰器在Django框架下去除冗余代码的教程
Python装饰器是一个消除冗余的强大工具。随着将功能模块化为大小合适的方法,即使是最复杂的工作流,装饰器也能使它变成简洁的功能。
例如让我们看看Djangoweb框架,该框架处理请求的方法接收一个方法对象,返回一个响应对象:
defhandle_request(request): returnHttpResponse("Hello,World")
我最近遇到一个案例,需要编写几个满足下述条件的api方法:
- 返回json响应
- 如果是GET请求,那么返回错误码
做为一个注册api端点例子,我将会像这样编写:
defregister(request): result=None #checkforpostonly ifrequest.method!='POST': result={"error":"thismethodonlyacceptsposts!"} else: try: user=User.objects.create_user(request.POST['username'], request.POST['email'], request.POST['password']) #optionalfields forfieldin['first_name','last_name']: iffieldinrequest.POST: setattr(user,field,request.POST[field]) user.save() result={"success":True} exceptKeyErrorase: result={"error":str(e)} response=HttpResponse(json.dumps(result)) if"error"inresult: response.status_code=500 returnresponse
然而这样我将会在每个api方法中编写json响应和错误返回的代码。这将会导致大量的逻辑重复。所以让我们尝试用装饰器实现DRY原则吧。
装饰器简介
如果你不熟悉装饰器,我可以简单解释一下,实际上装饰器就是有效的函数包装器,python解释器加载函数的时候就会执行包装器,包装器可以修改函数的接收参数和返回值。举例来说,如果我想要总是返回比实际返回值大一的整数结果,我可以这样写装饰器:
#adecoratorreceivesthemethodit'swrappingasavariable'f' defincrement(f): #weusearbitraryargsandkeywordsto #ensurewegraballtheinputarguments. defwrapped_f(*args,**kw): #notewecallfagainstthevariablespassedintothewrapper, #andcasttheresulttoanintandincrement. returnint(f(*args,**kw))+1 returnwrapped_f#thewrappedfunctiongetsreturned.
现在我们就可以用@符号和这个装饰器去装饰另外一个函数了:
@increment defplus(a,b): returna+b result=plus(4,6) assert(result==11,"Wewroteourdecoratorwrong!")
装饰器修改了存在的函数,将装饰器返回的结果赋值给了变量。在这个例子中,'plus'的结果实际指向increment(plus)的结果。
对于非post请求返回错误
现在让我们在一些更有用的场景下应用装饰器。如果在django中接收的不是POST请求,我们用装饰器返回一个错误响应。
defpost_only(f): """Ensuresamethodispostonly""" defwrapped_f(request): ifrequest.method!="POST": response=HttpResponse(json.dumps( {"error":"thismethodonlyacceptsposts!"})) response.status_code=500 returnresponse returnf(request) returnwrapped_f
现在我们可以在上述注册api中应用这个装饰器:
@post_only defregister(request): result=None try: user=User.objects.create_user(request.POST['username'], request.POST['email'], request.POST['password']) #optionalfields forfieldin['first_name','last_name']: iffieldinrequest.POST: setattr(user,field,request.POST[field]) user.save() result={"success":True} exceptKeyErrorase: result={"error":str(e)} response=HttpResponse(json.dumps(result)) if"error"inresult: response.status_code=500 returnresponse
现在我们就有了一个可以在每个api方法中重用的装饰器。
发送json响应
为了发送json响应(同时处理500状态码),我们可以新建另外一个装饰器:
defjson_response(f): """Returntheresponseasjson,andreturna500errorcodeifanerrorexists""" defwrapped(*args,**kwargs): result=f(*args,**kwargs) response=HttpResponse(json.dumps(result)) iftype(result)==dictand'error'inresult: response.status_code=500 returnresponse
现在我们就可以在原方法中去除json相关的代码,添加一个装饰器做为代替:
post_only @json_response defregister(request): try: user=User.objects.create_user(request.POST['username'], request.POST['email'], request.POST['password']) #optionalfields forfieldin['first_name','last_name']: iffieldinrequest.POST: setattr(user,field,request.POST[field]) user.save() return{"success":True} exceptKeyErrorase: return{"error":str(e)}
现在,如果我需要编写新的方法,那么我就可以使用装饰器做冗余的工作。如果我要写登录方法,我只需要写真正相关的代码:
@post_only @json_response deflogin(request): ifrequest.userisnotNone: return{"error":"Userisalreadyauthenticated!"} user=auth.authenticate(request.POST['username'],request.POST['password']) ifuserisnotNone: ifnotuser.is_active: return{"error":"Userisinactive"} auth.login(request,user) return{"success":True,"id":user.pk} else: return{"error":"Userdoesnotexistwiththosecredentials"}
BONUS:参数化你的请求方法
我曾经使用过Tubogears框架,其中请求参数直接解释转递给方法这一点我很喜欢。所以要怎样在Django中模仿这一特性呢?嗯,装饰器就是一种解决方案!
例如:
defparameterize_request(types=("POST",)): """ Parameterizetherequestinsteadofparsingtherequestdirectly. Onlythetypesspecifiedwillbeaddedtothequeryparameters. e.g.converta=test&b=cvinrequest.POSTto f(a=test,b=cv) """ defwrapper(f): defwrapped(request): kw={} if"GET"intypes: fork,vinrequest.GET.items(): kw[k]=v if"POST"intypes: fork,vinrequest.POST.items(): kw[k]=v returnf(request,**kw) returnwrapped returnwrapper
注意这是一个参数化装饰器的例子。在这个例子中,函数的结果是实际的装饰器。
现在我就可以用参数化装饰器编写方法了!我甚至可以选择是否允许GET和POST,或者仅仅一种请求参数类型。
@post_only @json_response @parameterize_request(["POST"]) defregister(request,username,email,password, first_name=None,last_name=None): user=User.objects.create_user(username,email,password) user.first_name=first_name user.last_name=last_name user.save() return{"success":True}
现在我们有了一个简洁的、易于理解的api。
BONUS#2:使用functools.wraps保存docstrings和函数名
很不幸,使用装饰器的一个副作用是没有保存方法名(__name__)和docstring(__doc__)值:
defincrement(f): """Incrementafunctionresult""" wrapped_f(a,b): returnf(a,b)+1 returnwrapped_f @increment defplus(a,b) """Addtwothingstogether""" returna+b plus.__name__#thisisnow'wrapped_f'insteadof'plus' plus.__doc__#thisnowreturns'Incrementafunctionresult'insteadof'Addtwothingstogether'
这将对使用反射的应用造成麻烦,比如Sphinx,一个自动生成文档的应用。
为了解决这个问题,我们可以使用'wraps'装饰器附加上名字和docstring:
fromfunctoolsimportwraps defincrement(f): """Incrementafunctionresult""" @wraps(f) wrapped_f(a,b): returnf(a,b)+1 returnwrapped_f @increment defplus(a,b) """Addtwothingstogether""" returna+b plus.__name__#thisreturns'plus' plus.__doc__#thisreturns'Addtwothingstogether'
BONUS#3:使用'decorator'装饰器
如果仔细看看上述使用装饰器的方式,在包装器声明和返回的地方也有不少重复。
你可以安装pythonegg'decorator',其中包含一个提供装饰器模板的'decorator'装饰器!
使用easy_install:
$sudoeasy_installdecorator
或者Pip:
$pipinstalldecorator
然后你可以简单的编写:
fromdecoratorimportdecorator @decorator defpost_only(f,request): """Ensuresamethodispostonly""" ifrequest.method!="POST": response=HttpResponse(json.dumps( {"error":"thismethodonlyacceptsposts!"})) response.status_code=500 returnresponse returnf(request)
这个装饰器更牛逼的一点是保存了__name__和__doc__的返回值,也就是它封装了functools.wraps的功能!