使用Python装饰器在Django框架下去除冗余代码的教程


Posted in Python onApril 16, 2015

 Python装饰器是一个消除冗余的强大工具。随着将功能模块化为大小合适的方法,即使是最复杂的工作流,装饰器也能使它变成简洁的功能。

例如让我们看看Django web框架,该框架处理请求的方法接收一个方法对象,返回一个响应对象:
 

def handle_request(request):
  return HttpResponse("Hello, World")

我最近遇到一个案例,需要编写几个满足下述条件的api方法:

  •     返回json响应
  •     如果是GET请求,那么返回错误码

做为一个注册api端点例子,我将会像这样编写:
 

def register(request):
  result = None
  # check for post only
  if request.method != 'POST':
    result = {"error": "this method only accepts posts!"}
  else:
    try:
      user = User.objects.create_user(request.POST['username'],
                      request.POST['email'],
                      request.POST['password'])
      # optional fields
      for field in ['first_name', 'last_name']:
        if field in request.POST:
          setattr(user, field, request.POST[field])
      user.save()
      result = {"success": True}
    except KeyError as e:
      result = {"error": str(e) }
  response = HttpResponse(json.dumps(result))
  if "error" in result:
    response.status_code = 500
  return response

然而这样我将会在每个api方法中编写json响应和错误返回的代码。这将会导致大量的逻辑重复。所以让我们尝试用装饰器实现DRY原则吧。

装饰器简介

如果你不熟悉装饰器,我可以简单解释一下,实际上装饰器就是有效的函数包装器,python解释器加载函数的时候就会执行包装器,包装器可以修改函数的接收参数和返回值。举例来说,如果我想要总是返回比实际返回值大一的整数结果,我可以这样写装饰器:
 

# a decorator receives the method it's wrapping as a variable 'f'
def increment(f):
  # we use arbitrary args and keywords to
  # ensure we grab all the input arguments.
  def wrapped_f(*args, **kw):
    # note we call f against the variables passed into the wrapper,
    # and cast the result to an int and increment .
    return int(f(*args, **kw)) + 1
  return wrapped_f # the wrapped function gets returned.

现在我们就可以用@符号和这个装饰器去装饰另外一个函数了:
 

@increment
def plus(a, b):
  return a + b
 
result = plus(4, 6)
assert(result == 11, "We wrote our decorator wrong!")

装饰器修改了存在的函数,将装饰器返回的结果赋值给了变量。在这个例子中,'plus'的结果实际指向increment(plus)的结果。

对于非post请求返回错误

现在让我们在一些更有用的场景下应用装饰器。如果在django中接收的不是POST请求,我们用装饰器返回一个错误响应。
 

def post_only(f):
  """ Ensures a method is post only """
  def wrapped_f(request):
    if request.method != "POST":
      response = HttpResponse(json.dumps(
        {"error": "this method only accepts posts!"}))
      response.status_code = 500
      return response
    return f(request)
  return wrapped_f

现在我们可以在上述注册api中应用这个装饰器:
 

@post_only
def register(request):
  result = None
  try:
    user = User.objects.create_user(request.POST['username'],
                    request.POST['email'],
                    request.POST['password'])
    # optional fields
    for field in ['first_name', 'last_name']:
      if field in request.POST:
        setattr(user, field, request.POST[field])
    user.save()
    result = {"success": True}
  except KeyError as e:
    result = {"error": str(e) }
  response = HttpResponse(json.dumps(result))
  if "error" in result:
    response.status_code = 500
  return response

现在我们就有了一个可以在每个api方法中重用的装饰器。

发送json响应

为了发送json响应(同时处理500状态码),我们可以新建另外一个装饰器:
 

def json_response(f):
  """ Return the response as json, and return a 500 error code if an error exists """
  def wrapped(*args, **kwargs):
    result = f(*args, **kwargs)
    response = HttpResponse(json.dumps(result))
    if type(result) == dict and 'error' in result:
      response.status_code = 500
return response

现在我们就可以在原方法中去除json相关的代码,添加一个装饰器做为代替:

post_only
@json_response
def register(request):
  try:
    user = User.objects.create_user(request.POST['username'],
                    request.POST['email'],
                    request.POST['password'])
    # optional fields
    for field in ['first_name', 'last_name']:
      if field in request.POST:
        setattr(user, field, request.POST[field])
    user.save()
    return {"success": True}
  except KeyError as e:
    return {"error": str(e) }

现在,如果我需要编写新的方法,那么我就可以使用装饰器做冗余的工作。如果我要写登录方法,我只需要写真正相关的代码:
 

@post_only
@json_response
def login(request):
  if request.user is not None:
    return {"error": "User is already authenticated!"}
  user = auth.authenticate(request.POST['username'], request.POST['password'])
  if user is not None:
    if not user.is_active:
      return {"error": "User is inactive"}
    auth.login(request, user)
    return {"success": True, "id": user.pk}
  else:
    return {"error": "User does not exist with those credentials"}

BONUS: 参数化你的请求方法

我曾经使用过Tubogears框架,其中请求参数直接解释转递给方法这一点我很喜欢。所以要怎样在Django中模仿这一特性呢?嗯,装饰器就是一种解决方案!

例如:
 

def parameterize_request(types=("POST",)):
  """
  Parameterize the request instead of parsing the request directly.
  Only the types specified will be added to the query parameters.
 
  e.g. convert a=test&b=cv in request.POST to
  f(a=test, b=cv)
  """
  def wrapper(f):
    def wrapped(request):
      kw = {}
      if "GET" in types:
        for k, v in request.GET.items():
          kw[k] = v
      if "POST" in types:
        for k, v in request.POST.items():
          kw[k] = v
      return f(request, **kw)
    return wrapped
  return wrapper

注意这是一个参数化装饰器的例子。在这个例子中,函数的结果是实际的装饰器。

现在我就可以用参数化装饰器编写方法了!我甚至可以选择是否允许GET和POST,或者仅仅一种请求参数类型。
 

@post_only
@json_response
@parameterize_request(["POST"])
def register(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__)值:
 

def increment(f):
  """ Increment a function result """
  wrapped_f(a, b):
    return f(a, b) + 1
  return wrapped_f
 
@increment
def plus(a, b)
  """ Add two things together """
  return a + b
 
plus.__name__ # this is now 'wrapped_f' instead of 'plus'
plus.__doc__  # this now returns 'Increment a function result' instead of 'Add two things together'

这将对使用反射的应用造成麻烦,比如Sphinx,一个 自动生成文档的应用。

为了解决这个问题,我们可以使用'wraps'装饰器附加上名字和docstring:
 

from functools import wraps
 
def increment(f):
  """ Increment a function result """
  @wraps(f)
  wrapped_f(a, b):
    return f(a, b) + 1
  return wrapped_f
 
@increment
def plus(a, b)
  """ Add two things together """
  return a + b
 
plus.__name__ # this returns 'plus'
plus.__doc__  # this returns 'Add two things together'

BONUS #3: 使用'decorator'装饰器

如果仔细看看上述使用装饰器的方式,在包装器声明和返回的地方也有不少重复。

你可以安装python egg 'decorator',其中包含一个提供装饰器模板的'decorator'装饰器!

使用easy_install:
 

$ sudo easy_install decorator

或者Pip:
 

$ pip install decorator

然后你可以简单的编写:

 

from decorator import decorator
 
@decorator
def post_only(f, request):
  """ Ensures a method is post only """
  if request.method != "POST":
    response = HttpResponse(json.dumps(
      {"error": "this method only accepts posts!"}))
    response.status_code = 500
    return response
  return f(request)

这个装饰器更牛逼的一点是保存了__name__和__doc__的返回值,也就是它封装了 functools.wraps的功能!

Python 相关文章推荐
pyside写ui界面入门示例
Jan 22 Python
用实例分析Python中method的参数传递过程
Apr 02 Python
python模拟登录并且保持cookie的方法详解
Apr 04 Python
Python高级用法总结
May 26 Python
Python全排列操作实例分析
Jul 24 Python
python: 判断tuple、list、dict是否为空的方法
Oct 22 Python
Python学习笔记之函数的参数和返回值的使用
Nov 20 Python
tensorflow 只恢复部分模型参数的实例
Jan 06 Python
搭建pypi私有仓库实现过程详解
Nov 25 Python
python连接mongodb数据库操作数据示例
Nov 30 Python
详解python3类型注释annotations实用案例
Jan 20 Python
利用Python判断整数是否是回文数的3种方法总结
Jul 07 Python
在服务器端实现无间断部署Python应用的教程
Apr 16 #Python
使用Protocol Buffers的C语言拓展提速Python程序的示例
Apr 16 #Python
使用Python编写一个模仿CPU工作的程序
Apr 16 #Python
利用Python中的mock库对Python代码进行模拟测试
Apr 16 #Python
使用Python脚本来控制Windows Azure的简单教程
Apr 16 #Python
在Python下利用OpenCV来旋转图像的教程
Apr 16 #Python
在Python中使用Neo4j数据库的教程
Apr 16 #Python
You might like
php实现可用于mysql,mssql,pg数据库操作类
2014/12/13 PHP
Yii2中YiiBase自动加载类、引用文件方法分析(autoload)
2016/07/25 PHP
Laravel 使用查询构造器配合原生sql语句查询的例子
2019/10/12 PHP
Laravel框架使用技巧之使用url()全局函数返回前一个页面的地址方法详解
2020/04/06 PHP
对联广告js flash激活
2006/10/19 Javascript
jQuery 源代码显示控件 (Ajax加载方式).
2009/05/18 Javascript
Javascript常考语句107条收集
2010/03/09 Javascript
模拟jQuery ajax服务器端与客户端通信的代码
2011/03/28 Javascript
StringTemplate遇见jQuery冲突的解决方法
2011/09/22 Javascript
javascript实现依次输入input自动定焦
2014/12/23 Javascript
Javascript中的几种URL编码方法比较
2015/01/23 Javascript
JavaScript检测上传文件大小的方法
2015/07/22 Javascript
JQUERY表单暂存功能插件分享
2016/02/23 Javascript
JavaScript判断页面加载完之后再执行预定函数的技巧
2016/05/17 Javascript
jQuery树控件zTree使用方法详解(一)
2017/02/28 Javascript
JS实现的简单表单验证功能示例
2017/10/13 Javascript
JavaScript递归函数解“汉诺塔”算法代码解析
2018/07/05 Javascript
新年快乐! javascript实现超级炫酷的3D烟花特效
2019/01/30 Javascript
基于js实现抽红包并分配代码实例
2019/09/19 Javascript
vue.js路由mode配置之去掉url上默认的#方法
2019/11/01 Javascript
python 巧用正则寻找字符串中的特定字符的位置方法
2018/05/02 Python
Python实现的合并两个有序数组算法示例
2019/03/04 Python
python实现12306登录并保存cookie的方法示例
2019/12/17 Python
Python利用 utf-8-sig 编码格式解决写入 csv 文件乱码问题
2020/02/21 Python
PageFactory设计模式基于python实现
2020/04/14 Python
Qoo10马来西亚:全球时尚和引领潮流的购物市场
2016/08/25 全球购物
中国网上药店领导者:1药网
2017/02/16 全球购物
英国街头品牌:Bee Inspired Clothing
2018/02/12 全球购物
英国设计的甲板鞋和船鞋:Chatham
2018/12/06 全球购物
计算机专业个人求职信范例
2013/09/23 职场文书
投标诚信承诺书
2014/05/26 职场文书
工作态度不端正检讨书
2014/10/04 职场文书
2015年财政局工作总结
2015/05/21 职场文书
vue3中的组件间通信
2021/03/31 Vue.js
源码分析Redis中 set 和 sorted set 的使用方法
2022/03/22 Redis
html,css,javascript是怎样变成页面的
2023/05/07 HTML / CSS