Python 装饰器实现DRY(不重复代码)原则


Posted in Python onMarch 05, 2018

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

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

现在我就可以用参数化装饰器编写方法了!我甚至可以选择是否允许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 相关文章推荐
windows系统中python使用rar命令压缩多个文件夹示例
May 06 Python
Python中的hypot()方法使用简介
May 18 Python
python命令行解析之parse_known_args()函数和parse_args()使用区别介绍
Jan 24 Python
Python列表推导式、字典推导式与集合推导式用法实例分析
Feb 07 Python
python利用ffmpeg进行录制屏幕的方法
Jan 10 Python
python3使用matplotlib绘制散点图
Mar 19 Python
Django框架文件上传与自定义图片上传路径、上传文件名操作分析
May 10 Python
Django生成PDF文档显示在网页上以及解决PDF中文显示乱码的问题
Jul 04 Python
Tensorflow实现部分参数梯度更新操作
Jan 23 Python
python dumps和loads区别详解
Feb 04 Python
Python如何爬取51cto数据并存入MySQL
Aug 25 Python
pdf论文中python画的图Type 3 fonts字体不兼容的解决方案
Apr 24 Python
Tensorflow实现卷积神经网络用于人脸关键点识别
Mar 05 #Python
python入门教程 python入门神图一张
Mar 05 #Python
详解TensorFlow在windows上安装与简单示例
Mar 05 #Python
python 中if else 语句的作用及示例代码
Mar 05 #Python
运用TensorFlow进行简单实现线性回归、梯度下降示例
Mar 05 #Python
tf.truncated_normal与tf.random_normal的详细用法
Mar 05 #Python
用tensorflow搭建CNN的方法
Mar 05 #Python
You might like
如何使用Strace调试工具
2013/06/03 PHP
PHP+shell实现多线程的方法
2015/07/01 PHP
两种php给图片加水印的实现代码
2020/04/18 PHP
Zend Framework教程之路由功能Zend_Controller_Router详解
2016/03/07 PHP
Javascript remove 自定义数组删除方法
2009/10/20 Javascript
javascript中的onkeyup和onkeydown区别介绍
2013/04/28 Javascript
禁止选中文字兼容IE、Chrome、FF等
2013/09/04 Javascript
jquery不常用方法汇总
2015/07/26 Javascript
ES6教程之for循环和Map,Set用法分析
2017/04/10 Javascript
利用canvas中toDataURL()将图片转为dataURL(base64)的方法详解
2017/11/20 Javascript
在小程序/mpvue中使用flyio发起网络请求的方法
2018/09/13 Javascript
vue使用Font Awesome的方法步骤
2019/02/26 Javascript
layui table表格数据的新增,修改,删除,查询,双击获取行数据方式
2019/11/14 Javascript
小程序瀑布流组件实现翻页与图片懒加载
2020/05/19 Javascript
JavaScript事件循环及宏任务微任务原理解析
2020/09/02 Javascript
Python语言编写电脑时间自动同步小工具
2013/03/08 Python
在Python的Django框架的视图中使用Session的方法
2015/07/23 Python
Python的网络编程库Gevent的安装及使用技巧
2016/06/24 Python
python3.x上post发送json数据
2018/03/04 Python
Python 装饰器@,对函数进行功能扩展操作示例【开闭原则】
2019/10/17 Python
Windows10下Tensorflow2.0 安装及环境配置教程(图文)
2019/11/21 Python
HTML5 canvas画图并保存成图片的jcanvas插件
2014/01/17 HTML / CSS
美国在线健康和美容市场:Pharmapacks
2018/12/05 全球购物
IMPORT的选项IGNORE有什么作用?缺省是什么设置?
2015/09/17 面试题
J2EE相关知识面试题
2013/08/26 面试题
介绍一下SOA和SOA的基本特征
2016/02/24 面试题
财务会计应届生求职信
2013/11/24 职场文书
导游实习生自荐书
2014/01/28 职场文书
新员工试用期自我鉴定
2014/04/17 职场文书
项目经理任命书内容
2014/06/06 职场文书
教师节横幅标语
2014/10/08 职场文书
医生个人年终总结
2015/02/28 职场文书
2015年幼儿园中班下学期工作总结
2015/05/22 职场文书
元旦晚会开场白
2015/05/29 职场文书
Python实现批量自动整理文件
2022/03/16 Python
Python中itertools库的四个函数介绍
2022/04/06 Python