Python的装饰器模式与面向切面编程详解


Posted in Python onJune 21, 2015

今天来讨论一下装饰器。装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

1. 装饰器入门

1.1. 需求是怎么来的?

装饰器的定义很是抽象,我们来看一个小例子。

def foo():

    print 'in foo()'

 

foo()

这是一个很无聊的函数没错。但是突然有一个更无聊的人,我们称呼他为B君,说我想看看执行这个函数用了多长时间,好吧,那么我们可以这样做:

import time

def foo():

    start = time.clock()

    print 'in foo()'

    end = time.clock()

    print 'used:', end - start

 

foo()

很好,功能看起来无懈可击。可是蛋疼的B君此刻突然不想看这个函数了,他对另一个叫foo2的函数产生了更浓厚的兴趣。

怎么办呢?如果把以上新增加的代码复制到foo2里,这就犯了大忌了~复制什么的难道不是最讨厌了么!而且,如果B君继续看了其他的函数呢?

1.2. 以不变应万变,是变也

还记得吗,函数在Python中是一等公民,那么我们可以考虑重新定义一个函数timeit,将foo的引用传递给他,然后在timeit中调用foo并进行计时,这样,我们就达到了不改动foo定义的目的,而且,不论B君看了多少个函数,我们都不用去修改函数定义了!

import time

 

def foo():

    print 'in foo()'

 

def timeit(func):

    start = time.clock()

    func()

    end =time.clock()

    print 'used:', end - start

 

timeit(foo)

看起来逻辑上并没有问题,一切都很美好并且运作正常!……等等,我们似乎修改了调用部分的代码。原本我们是这样调用的:foo(),修改以后变成了:timeit(foo)。这样的话,如果foo在N处都被调用了,你就不得不去修改这N处的代码。或者更极端的,考虑其中某处调用的代码无法修改这个情况,比如:这个函数是你交给别人使用的。

1.3. 最大限度地少改动!

既然如此,我们就来想想办法不修改调用的代码;如果不修改调用代码,也就意味着调用foo()需要产生调用timeit(foo)的效果。我们可以想到将timeit赋值给foo,但是timeit似乎带有一个参数……想办法把参数统一吧!如果timeit(foo)不是直接产生调用效果,而是返回一个与foo参数列表一致的函数的话……就很好办了,将timeit(foo)的返回值赋值给foo,然后,调用foo()的代码完全不用修改!

#-*- coding: UTF-8 -*-

import time

 

def foo():

    print 'in foo()'

 

# 定义一个计时器,传入一个,并返回另一个附加了计时功能的方法

def timeit(func):

     

    # 定义一个内嵌的包装函数,给传入的函数加上计时功能的包装

    def wrapper():

        start = time.clock()

        func()

        end =time.clock()

        print 'used:', end - start

     

    # 将包装后的函数返回

    return wrapper

 

foo = timeit(foo)

foo()

这样,一个简易的计时器就做好了!我们只需要在定义foo以后调用foo之前,加上foo = timeit(foo),就可以达到计时的目的,这也就是装饰器的概念,看起来像是foo被timeit装饰了。在在这个例子中,函数进入和退出时需要计时,这被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。与传统编程习惯的从上往下执行方式相比较而言,像是在函数执行的流程中横向地插入了一段逻辑。在特定的业务领域里,能减少大量重复代码。面向切面编程还有相当多的术语,这里就不多做介绍,感兴趣的话可以去找找相关的资料。

这个例子仅用于演示,并没有考虑foo带有参数和有返回值的情况,完善它的重任就交给你了 :)

2. Python的额外支持

2.1. 语法糖

上面这段代码看起来似乎已经不能再精简了,Python于是提供了一个语法糖来降低字符输入量。

import time

 

def timeit(func):

    def wrapper():

        start = time.clock()

        func()

        end =time.clock()

        print 'used:', end - start

    return wrapper

 

@timeit

def foo():

    print 'in foo()'

 

foo()

重点关注第11行的@timeit,在定义上加上这一行与另外写foo = timeit(foo)完全等价,千万不要以为@有另外的魔力。除了字符输入少了一些,还有一个额外的好处:这样看上去更有装饰器的感觉。

2.2. 内置的装饰器

内置的装饰器有三个,分别是staticmethod、classmethod和property,作用分别是把类中定义的实例方法变成静态方法、类方法和类属性。由于模块里可以定义函数,所以静态方法和类方法的用处并不是太多,除非你想要完全的面向对象编程。而属性也不是不可或缺的,Java没有属性也一样活得很滋润。从我个人的Python经验来看,我没有使用过property,使用staticmethod和classmethod的频率也非常低。

class Rabbit(object):

     

    def __init__(self, name):

        self._name = name

     

    @staticmethod

    def newRabbit(name):

        return Rabbit(name)

     

    @classmethod

    def newRabbit2(cls):

        return Rabbit('')

     

    @property

    def name(self):

        return self._name

这里定义的属性是一个只读属性,如果需要可写,则需要再定义一个setter:

@name.setter

def name(self, name):

    self._name = name

2.3. functools模块

functools模块提供了两个装饰器。这个模块是Python 2.5后新增的,一般来说大家用的应该都高于这个版本。但我平时的工作环境是2.4 T-T

2.3.1. wraps(wrapped[, assigned][, updated]):

这是一个很有用的装饰器。看过前一篇反射的朋友应该知道,函数是有几个特殊属性比如函数名,在被装饰后,上例中的函数名foo会变成包装函数的名字wrapper,如果你希望使用反射,可能会导致意外的结果。这个装饰器可以解决这个问题,它能将装饰过的函数的特殊属性保留。

import time

import functools

 

def timeit(func):

    @functools.wraps(func)

    def wrapper():

        start = time.clock()

        func()

        end =time.clock()

        print 'used:', end - start

    return wrapper

 

@timeit

def foo():

    print 'in foo()'

 

foo()

print foo.__name__

首先注意第5行,如果注释这一行,foo.__name__将是'wrapper'。另外相信你也注意到了,这个装饰器竟然带有一个参数。实际上,他还有另外两个可选的参数,assigned中的属性名将使用赋值的方式替换,而updated中的属性名将使用update的方式合并,你可以通过查看functools的源代码获得它们的默认值。对于这个装饰器,相当于wrapper = functools.wraps(func)(wrapper)。

2.3.2. total_ordering(cls):

这个装饰器在特定的场合有一定用处,但是它是在Python 2.7后新增的。它的作用是为实现了至少__lt__、__le__、__gt__、__ge__其中一个的类加上其他的比较方法,这是一个类装饰器。如果觉得不好理解,不妨仔细看看这个装饰器的源代码:

def total_ordering(cls):

      """Class decorator that fills in missing ordering methods"""

      convert = {

          '__lt__': [('__gt__', lambda self, other: other < self),

                     ('__le__', lambda self, other: not other < self),

                     ('__ge__', lambda self, other: not self < other)],

          '__le__': [('__ge__', lambda self, other: other <= self),

                     ('__lt__', lambda self, other: not other <= self),

                     ('__gt__', lambda self, other: not self <= other)],

          '__gt__': [('__lt__', lambda self, other: other > self),

                     ('__ge__', lambda self, other: not other > self),

                     ('__le__', lambda self, other: not self > other)],

          '__ge__': [('__le__', lambda self, other: other >= self),

                     ('__gt__', lambda self, other: not other >= self),

                     ('__lt__', lambda self, other: not self >= other)]

      }

      roots = set(dir(cls)) & set(convert)

      if not roots:

          raise ValueError('must define at least one ordering operation: < > <= >=')

      root = max(roots)       # prefer __lt__ to __le__ to __gt__ to __ge__

      for opname, opfunc in convert[root]:

          if opname not in roots:

              opfunc.__name__ = opname

              opfunc.__doc__ = getattr(int, opname).__doc__

              setattr(cls, opname, opfunc)

      return cls

本文到这里就全部结束了,有空的话我会整理一个用于检查参数类型的装饰器的源代码放上来,算是一个应用吧 :)

Python 相关文章推荐
Python利用turtle库绘制彩虹代码示例
Dec 20 Python
使用python 爬虫抓站的一些技巧总结
Jan 10 Python
python操作excel文件并输出txt文件的实例
Jul 10 Python
python修改txt文件中的某一项方法
Dec 29 Python
python使用Matplotlib改变坐标轴的默认位置
Oct 18 Python
Centos7 下安装最新的python3.8
Oct 28 Python
python3实现网页版raspberry pi(树莓派)小车控制
Feb 12 Python
解决Jupyter notebook中.py与.ipynb文件的import问题
Apr 21 Python
python多进程下的生产者和消费者模型
May 07 Python
python实现126邮箱发送邮件
May 20 Python
Python selenium 加载并保存QQ群成员,去除其群主、管理员信息的示例代码
May 28 Python
解决Django transaction进行事务管理踩过的坑
Apr 24 Python
Python安装第三方库的3种方法
Jun 21 #Python
Python实现线程池代码分享
Jun 21 #Python
Python os模块学习笔记
Jun 21 #Python
Pthon批量处理将pdb文件生成dssp文件
Jun 21 #Python
Python实现删除文件但保留指定文件
Jun 21 #Python
Python ValueError: invalid literal for int() with base 10 实用解决方法
Jun 21 #Python
让Python代码更快运行的5种方法
Jun 21 #Python
You might like
PHP令牌 Token改进版
2008/07/18 PHP
PHP中文处理 中文字符串截取(mb_substr)和获取中文字符串字数
2011/11/10 PHP
PHP数组 为文章加关键字连接 文章内容自动加链接
2011/12/29 PHP
深入理解 PHP7 中全新的 zval 容器和引用计数机制
2018/10/15 PHP
PHP扩展Swoole实现实时异步任务队列示例
2019/04/13 PHP
一段多浏览器的&quot;复制到剪贴板&quot;javascript代码
2007/03/27 Javascript
Jquery实现图片放大镜效果的思路及代码(自写)
2013/10/18 Javascript
jquery.post用法之type设置问题
2014/02/24 Javascript
jQuery 选择器详解
2015/01/19 Javascript
JS与HTML结合使用marquee标签实现无缝滚动效果代码
2016/07/05 Javascript
jQuery web 组件 后台日历价格、库存设置的代码
2016/10/14 Javascript
探究JavaScript中的五种事件处理程序方式
2016/12/07 Javascript
Bootstrap布局之栅格系统学习笔记
2017/05/04 Javascript
Node.js 使用request模块下载文件的实例
2018/09/05 Javascript
微信小程序中使用Async-await方法异步请求变为同步请求方法
2019/03/28 Javascript
了解JavaScript中的选择器
2019/05/24 Javascript
微信小程序的授权实现过程解析
2019/08/02 Javascript
快速对接payjq的个人微信支付接口过程解析
2019/08/15 Javascript
对layer弹出框中icon数字参数的说明介绍
2019/09/04 Javascript
layui动态加载多表头的实例
2019/09/05 Javascript
Django查找网站项目根目录和对正则表达式的支持
2015/07/15 Python
对python自动生成接口测试的示例讲解
2018/11/30 Python
Python自动发送邮件的方法实例总结
2018/12/08 Python
选择python进行数据分析的理由和优势
2019/06/25 Python
django 解决自定义序列化返回处理数据为null的问题
2020/05/20 Python
pytorch 查看cuda 版本方式
2020/06/23 Python
小学生爱国演讲稿
2014/04/25 职场文书
档案保密承诺书
2014/06/03 职场文书
2014乡镇机关党员个人对照检查材料思想汇报
2014/10/09 职场文书
师德师风整改措施
2014/10/24 职场文书
2015学生会文艺部工作总结
2015/04/03 职场文书
台风停课通知
2015/04/24 职场文书
主题班会开场白
2015/06/01 职场文书
元旦联欢晚会主持词
2015/07/01 职场文书
apache基于端口创建虚拟主机的示例
2021/04/24 Servers
Java Spring Boot请求方式与请求映射过程分析
2022/06/25 Java/Android