Python装饰器由浅入深

装饰器的功能在很多语言中都有,名字也不尽相同,其实它体现的是一种设计模式,强调的是开放封闭原则,更多的用于后期功能升级而不是编写新的代码。装饰器不光能装饰函数,也能装饰其他的对象,比如类,但通常,我们以装饰函数为例子介绍其用法。要理解在Python中装饰器的原理,需要一步一步来。本文尽量描述得浅显易懂,从最基础的内容讲起。

  (注:以下使用Python3.5.1环境)

一、Python的函数相关基础

  第一,必须强调的是python是从上往下顺序执行的,而且碰到函数的定义代码块是不会立即执行它的,只有等到该函数被调用时,才会执行其内部的代码块。

1 def foo():
2     print("foo函数被运行了!")
3
4
5 如果就这么样,foo里的语句是不会被执行的。
6 程序只是简单的将定义代码块读入内存中。

  再看看,顺序执行的例子:

 1 def foo():
 2     print("我是上面的函数定义!")
 3
 4 def foo():
 5     print("我是下面的函数定义!")
 6
 7 foo()
 8
 9 运行结果:
10
11 我是下面的函数定义

  可见,因为顺序执行的原因,下面的foo将上面的foo覆盖了。因此,在Python中代码的放置位置是有要求的,不能随意摆放,函数体要放在被调用的语句之前。

如果你想学习Python可以来这个群,首先是四七二,中间是三零九,最后是二六一,里面有大量的学习资料可以下载。

  其次,我们还要先搞清楚几样东西:函数名、函数体、返回值,函数的内存地址、函数名加括号、函数名被当作参数、函数名加括号被当作参数、返回函数名、返回函数名加括号。对于如下的函数:

1 def foo():
2     print("让我们干点啥!")
3     return "ok"
4
5 foo()

  函数名:        foo

  函数体:        第1-3行

  返回值:        字符串“ok”    如果不显式给出return的对象,那么默认返回None

  函数的内存地址:    当函数体被读进内存后的保存位置,它由标识符即函数名foo引用,也就是说foo指向的是函数体在内存内的保存位置。

  函数名加括号:       例如foo(),函数的调用方法,只有见到这个括号,程序会根据函数名从内存中找到函数体,然后执行它

  再看下面这个例子:

1 def outer(func):
2     def inner():
3         print("我是内层函数!")
4     return inner
5
6 def foo():
7     print("我是原始函数!")
8
9 outer(foo)
10 outer(foo())

  在python中,一切都是对象,函数也不例外。因此可以将函数名,甚至函数名加括号进行调用的方式作为另一个函数的返回值。上面代码中,outer和foo是两个函数,outer(foo)表示将foo函数的函数名当做参数传递给outer函数并执行outer函数;outer(foo())表示将foo函数执行后的返回值当做参数传递给outer函数并执行outer函数,由于foo函数没有指定返回值,实际上它传递给了outer函数一个None。注意其中的差别,有没有括号是关键!

  同样,在outer函数内部,返回了一个inner,它是在outer函数内部定义的一个函数,注意,由于inner后面没有加括号,所以返回的是inner的函数体,实际上也就是inner这个名字,一个简单的引用而已。那么,如果outer函数返回的是inner()呢?现在你应该已经很清楚了,它会先执行inner函数的内容,然后返回个None给outer,outer再把这个None返回给调用它的对象。

  请记住,函数名、函数加括号可以被当做参数传递,也可以被当做返回值return,有没有括号是两个截然不同的意思!


二、装饰器的使用场景

  装饰器通常用于在不改变原有函数代码和功能的情况下,为其添加额外的功能。比如在原函数执行前先执行点什么,在执行后执行点什么。

  让我们通过一个例子来看看,装饰器的使用场景和体现的设计模式。(抱歉的是我设计不出更好的场景,只能引用武大神的案例加以演绎)

  有一个大公司,下属的基础平台部负责内部应用程序及API的开发,有上百个业务部门负责不同的业务,他们各自调用基础平台部提供的不同函数处理自己的业务,情况如下:  

 1 # 基础平台部门开发了上百个函数
 2 def f1():
 3     print("业务部门1数据接口......")
 4 def f2():
 5     print("业务部门2数据接口......")
 6 def f3():
 7     print("业务部门3数据接口......")
 8 def f100():
 9     print("业务部门100数据接口......")
10
11 #各部门分别调用
12 f1()
13 f2()
14 f3()
15 f100()

  由于公司在创业初期,基础平台部开发这些函数时,由于各种原因,比如时间,比如考虑不周等等,没有为函数调用进行安全认证。现在,平台部主管决定弥补这个缺陷,于是:

  第一回,主管叫来了一个运维工程师,工程师跑上跑下逐个部门进行通知,让他们在代码里加上认证功能,然而,当天他被开除了。

  第二回:主管又叫来了一个运维工程师,工程师用shell写了个复杂的脚本,勉强实现了功能。但他很快就回去接着做运维了,不会开发的运维不是好运维....

  第三回:主管叫来了一个python自动化开发工程师,哥们是这么干的:只对基础平台的代码进行重构,让N个业务部门无需做任何修改。这哥们很快也被开了,连运维也没得做。  

def f1():
    #加入认证程序代码
    print("业务部门1数据接口......")
def f2():
    # 加入认证程序代码
    print("业务部门2数据接口......")
def f3():
    # 加入认证程序代码
    print("业务部门3数据接口......")
def f100():
    #加入认证程序代码
    print("业务部门100数据接口......")

#各部门分别调用
f1()
f2()
f3()
f100()

  第四回:主管又换了个 工程师,他是这么干的:定义个认证函数,原来其他的函数调用它,代码如下框。但是,主管依然不满意,不过这一次他解释了为什么。主管说:写代码要遵循开放封闭原则,虽然在这个原则主要是针对面向对象开发,但是也适用于函数式编程,简单来说,它规定已经实现的功能代码内部不允许被修改,但外部可以被扩展,即:封闭:已实现的功能代码块;开放:对扩展开放。如果将开放封闭原则应用在上述需求中,那么就不允许在函数 f1 、f2、f3......f100的内部进行代码修改。遗憾的是,工程师没有漂亮的女朋友,所以很快也被开除了。

def login():
    print("认证成功!")

def f1():
    login()
    print("业务部门1数据接口......")
def f2():
    login()
    print("业务部门2数据接口......")
def f3():
    login()
    print("业务部门3数据接口......")
def f100():
    login()
    print("业务部门100数据接口......")

#各部门分别调用
f1()
f2()
f3()
f100()

  第五回:已经没有时间让主管找别人来干这活了,他决定亲自上阵,并且打算在函数执行后再增加个日志功能。主管是这么想的:不会装饰器的主管不是好码农!要不为啥我能当主管,你只能被管呢?嘿嘿。他的代码如下:

#/usr/bin/env python
#coding:utf-8

def outer(func):
    def inner():
        print("认证成功!")
        result = func()
        print("日志添加成功")
        return result
    return inner

@outer
def f1():
    print("业务部门1数据接口......")

@outer
def f2():
    print("业务部门2数据接口......")
@outer
def f3():
    print("业务部门3数据接口......")

@outer
def f100():
    print("业务部门100数据接口......")

#各部门分别调用
f1()
f2()
f3()
f100()

  对于上述代码,也是仅需对基础平台的代码进行拓展,就可以实现在其他部门调用函数 f1 f2 f3 f100 之前都进行认证操作,在操作结束后保存日志,并且其他业务部门无需他们自己的代码做任何修改,调用方式也不用变。“主管”写完代码后,觉得独乐了不如众乐乐,打算显摆一下,于是写了篇博客将过程进行了详细的说明。


 三、装饰器的内部原理

  下面我们以f1函数为例进行说明:

def outer(func):
    def inner():
        print("认证成功!")
        result = func()
        print("日志添加成功")
        return result
    return inner

@outer
def f1():
    print("业务部门1数据接口......")

  运用我们在第一部分介绍的知识来分析一下上面这段代码:

  1. 程序开始运行,从上往下编译,读到def outer(func):的时候,发现这是个“一等公民”->函数,于是把函数体加载到内存里,然后过。
  2. 读到@outer的时候,程序被@这个语法糖吸引住了,知道这是个装饰器,按规矩要立即执行的,于是程序开始运行@后面那个名字outer所定义的函数。(相信没有人会愚蠢的将@outer写到别的位置,它只能放在被装饰的函数的上方最近处,不要空行。)
  3. 程序返回到outer函数,开始执行装饰器的语法规则,这部分规则是定死的,是python的“法律”,不要问为什么。规则是:被装饰的函数的名字会被当作参数传递给装饰函数。装饰函数执行它自己内部的代码后,会将它的返回值赋值给被装饰的函数。

  如下图所示:

  

  这里面需要注意的是:

  •   @outer和@outer()有区别,没有括号时,outer函数依然会被执行,这和传统的用括号才能调用函数不同,需要特别注意!那么有括号呢?那是装饰器的高级用法了,以后会介绍。
  •   是f1这个函数名(而不是f1()这样被调用后)当做参数传递给装饰函数outer,也就是:func = f1,@outer等于outer(f1),实际上传递了f1的函数体,而不是执行f1后的返回值。
  •       outer函数return的是inner这个函数名,而不是inner()这样被调用后的返回值。

  如果你对第一部分函数的基础知识有清晰的了解,那么上面的内容你应该很容易理解。

  4. 程序开始执行outer函数内部的内容,一开始它又碰到了一个函数,很绕是吧?当然,你可以在 inner函数前后安排点别的代码,但它们不是重点,而且有点小麻烦,下面会解释。inner函数定义块被程序观察到后不会立刻执行,而是读入内存中(这是潜规则)。

  5. 再往下,碰到return inner,返回值是个函数名,并且这个函数名会被赋值给f1这个被装饰的函数,也就是f1 = inner。根据前面的知识,我们知道,此时f1函数被新的函数inner覆盖了(实际上是f1这个函数名更改成指向inner这个函数名指向的函数体内存地址,f1不再指向它原来的函数体的内存地址),再往后调用f1的时候将执行inner函数内的代码,而不是先前的函数体。那么先前的函数体去哪了?还记得我们将f1当做参数传递给func这个形参么?func这个变量保存了老的函数在内存中的地址,通过它就可以执行 老的函数体,你能在inner函数里看到result = func()这句代码,它就是这么干的!

  6.接下来,还没有结束。当业务部门,依然通过f1()的方式调用f1函数时,执行的就不再是老的f1函数的代码,而是inner函数的代码。在本例中,它首先会打印个“认证成功”的提示,很显然你可以换成任意的代码,这只是个示例;然后,它会执行func函数并将返回值赋值个变量result,这个func函数就是老的f1函数;接着,它又打印了“日志保存”的提示,这也只是个示例,可以换成任何你想要的;最后返回result这个变量。我们在业务部门的代码上可以用 r = f1()的方式接受result的值。

  7.以上流程走完后,你应该看出来了,在没有对业务部门的代码和接口调用方式做任何修改的同时,也没有对基础平台部原有的代码做内部修改,仅仅是添加了一个装饰函数,就实现了我们的需求,在函数调用前先认证,调用后写入日志。这就是装饰器的最大作用。

  问题:那么为什么我们要搞一个outer函数一个inner函数这么复杂呢?一层函数不行吗?

  答:请注意,@outer这句代码在程序执行到这里的时候就会自动执行outer函数内部的代码,如果不封装一下,在业务部门还未进行调用的时候,就执行了些什么,这和初衷有点不符。当然,如果你对这个有需求也不是不行。请看下面的例子,它只有一层函数。

def outer(func):
        print("认证成功!")
        result = func()
        print("日志添加成功")
        return result

@outer
def f1():

    print("业务部门1数据接口......")

# 业务部门并没有开始执行f1函数

执行结果:

认证成功!
业务部门1数据接口......
日志添加成功

  看到没?我只是定义好了函数,业务部门还没有调用f1函数呢,程序就把工作全做了。这就是封装一层函数的原因。

 



 

四、装饰器的参数传递

  细心的朋友可能已经发现了,上面的例子中,f1函数没有参数,在实际情况中肯定会需要参数的,那参数怎么传递的呢?

  一个参数的情况:

def outer(func):
    def inner(username):
        print("认证成功!")
        result = func(username)
        print("日志添加成功")
        return result
    return inner

@outer
def f1(name):

    print("%s 正在连接业务部门1数据接口......"%name)

# 调用方法
f1("jack")

  在inner函数的定义部分也加上一个参数,调用func函数的时候传递这个参数,很好理解吧?可问题又来了,那么另外一个部门调用的f2有2个参数呢?f3有3个参数呢?你怎么传递?

  很简单,我们有*args和**kwargs嘛!号称“万能参数”!简单修改一下上面的代码:

def outer(func):
    def inner(*args,**kwargs):
        print("认证成功!")
        result = func(*args,**kwargs)
        print("日志添加成功")
        return result
    return inner

@outer
def f1(name,age):

    print("%s 正在连接业务部门1数据接口......"%name)

# 调用方法
f1("jack",18)


五、更进一步的思考

  一个函数可以被多个函数装饰吗?可以的!看下面的例子!  

def outer1(func):
    def inner(*args,**kwargs):
        print("认证成功!")
        result = func(*args,**kwargs)
        print("日志添加成功")
        return result
    return inner

def outer2(func):
    def inner(*args,**kwargs):
        print("一条欢迎信息。。。")
        result = func(*args,**kwargs)
        print("一条欢送信息。。。")
        return result
    return inner

@outer1
@outer2
def f1(name,age):

    print("%s 正在连接业务部门1数据接口......"%name)

# 调用方法
f1("jack",18)

执行结果:

认证成功!
一条欢迎信息。。。
jack 正在连接业务部门1数据接口......
一条欢送信息。。。
日志添加成功

  更进一步的,装饰器自己可以有参数吗?可以的!看下面的例子:

# 认证函数
def  auth(request,kargs):
    print("认证成功!")
# 日志函数
def log(request,kargs):
    print("日志添加成功")
# 装饰器函数。接收两个参数,这两个参数应该是某个函数的名字。
def Filter(auth_func,log_func):
    # 第一层封装,f1函数实际上被传递给了main_fuc这个参数
    def outer(main_func):
        # 第二层封装,auth和log函数的参数值被传递到了这里
        def wrapper(request,kargs):
            # 下面代码的判断逻辑不重要,重要的是参数的引用和返回值
            before_result = auth(request,kargs)
            if(before_result != None):
                return before_result;

            main_result = main_func(request,kargs)
            if(main_result != None):
                return main_result;

            after_result = log(request,kargs)
            if(after_result != None):
                return after_result;

        return wrapper
    return outer
# 注意了,这里的装饰器函数有参数哦,它的意思是先执行filter函数
# 然后将filter函数的返回值作为装饰器函数的名字返回到这里,所以,
# 其实这里,Filter(auth,log) = outer , @Filter(auth,log) =  @outer
@Filter(auth,log)
def f1(name,age):

    print("%s 正在连接业务部门1数据接口......"%name)

# 调用方法
f1("jack",18)

运行结果:

认证成功!
jack 正在连接业务部门1数据接口......
日志添加成功

  又绕晕了?其实你可以这么理解,先执行Filter函数,获得它的返回值outer,再执行@outer装饰器语法。



 

   看到这,是不是觉得自己已经天下无敌了,有种装饰器尽在我手的感觉?

时间: 2024-08-03 04:35:55

Python装饰器由浅入深的相关文章

详解Python装饰器由浅入深_python

装饰器的功能在很多语言中都有,名字也不尽相同,其实它体现的是一种设计模式,强调的是开放封闭原则,更多的用于后期功能升级而不是编写新的代码.装饰器不光能装饰函数,也能装饰其他的对象,比如类,但通常,我们以装饰函数为例子介绍其用法.要理解在Python中装饰器的原理,需要一步一步来.本文尽量描述得浅显易懂,从最基础的内容讲起. (注:以下使用Python3.5.1环境) 一.Python的函数相关基础 第一,必须强调的是python是从上往下顺序执行的,而且碰到函数的定义代码块是不会立即执行它的,只

对Python 装饰器的理解心得

最近写一个py脚本来整理电脑中的文档,其中需要检校输入的字符,为了不使代码冗长,想到使用装 饰器. 上网搜索有关python的装饰器学习文档,主要看的是AstralWind的一篇博文,以及Limodou的一篇文章 .作为初学者,这两篇文章对新手有很大的帮助,但仍然有些不易理解的地方.因此在此以一个初学者的 认知记录一下python的装饰器的学习心得. 1. 什么是装饰器? 顾名思义,装饰器就是在方法上方标一个带有@符号的方法名,以此来对被装饰的方法进行点缀改造. 当你明白什么是装饰器之后,自然会

Python装饰器详解

装饰器(decorator)是一种高级Python语法.装饰器可以对一个函数.方法或者类进行加工.在Python中,我们有多种方法对函数和类进行加工,比如在Python闭包中,我们见到函数对象作为某一个函数的返回结果.相对于其它方式,装饰器语法简单,代码可读性高.因此,装饰器在Python项目中有广泛的应用. 装饰器最早在Python 2.5中出现,它最初被用于加工函数和方法这样的可调用对象(callable object,这样的对象定义有__call__方法).在Python 2.6以及之后的

Python装饰器使用实例:验证参数合法性

          这篇文章主要介绍了Python装饰器使用实例:验证参数合法性,本文直接给出代码实例,代码中包含详细注释,需要的朋友可以参考下               python是不带静态检查的动态语言,有时候需要在调用函数时保证参数合法.检查参数合法性是一个显著的切面场景,各个函数都可能有这个需求.但另一方面,参数合法性是不是应该由调用方来保证比较好也是一个需要结合实际才能回答的问题,总之双方约定好,不要都不检查或者都检查就可以了.下面这个模块用于在函数上使用装饰器进行参数的合法性验证

对于Python装饰器使用的一些建议

  这篇文章主要介绍了对于Python装饰器使用的一些建议,装饰器是Python学习进阶中的重要知识,需要的朋友可以参考下 装饰器基本概念 大家都知道装饰器是一个很著名的设计模式,经常被用于 AOP (面向切面编程)的场景,较为经典的有插入日志,性能测试,事务处理,Web权限校验, Cache等. Python 语言本身提供了装饰器语法(@),典型的装饰器实现如下: ? 1 2 3 @function_wrapper def function(): pass @实际上是 python2.4 才提

九步学会Python装饰器

  本文实例讲述了Python装饰器.分享给大家供大家参考.具体分析如下: 这是在Python学习小组上介绍的内容,现学现卖.多练习是好的学习方式. 第一步:最简单的函数,准备附加额外功能 ? 1 2 3 4 5 6 # -*- coding:gbk -*- '''示例1: 最简单的函数,表示调用了两次''' def myfunc(): print("myfunc() called.") myfunc() myfunc() 第二步:使用装饰函数在函数执行前和执行后分别附加额外功能 ?

对Python装饰器的个人理解方法

0.说明                   在自己好好总结并对Python装饰器的执行过程进行分解之前,对于装饰器虽然理解它的基本工作方式,但对于存在复杂参数的装饰器(装饰器和函数本身都有参数),总是会感到很模糊,即使这会弄懂了,下一次也很快忘记,其实本质上还是没有多花时间去搞懂其中的细节问题.         虽然网络上已经有很多这样的文章,但显然都是别人的思想,因此自己总是记不牢,所以花点时间自己好好整理一下.         最近在对<Python核心编程>做总结,收获了不少,下面分享

多个装饰器-Python 装饰器的疑问?

问题描述 Python 装饰器的疑问? 在学装饰器的时候看到这篇文章,[http://blog.csdn.net/mdl13412/article/details/22608283浅谈python闭包].感觉很赞.但在最后部分"多个装饰器运行期行为"时糊涂了. 自己用下列程序桌布验证时发现了问题. def tracer(msg): print "[TRACE] %s" % msg def login_debug_helper(show_debug_info=Fals

【Python之旅】第四篇(一):Python装饰器

有时候拿到一个程序接口,需要对其进行扩展,但是又不能修改原来接口的源代码,这时候就需要使用装饰器了.     有下面一个小程序,假如是别人提供给我们的调用接口: 1 2 3 4 5 import time   def sayHi():         time.sleep(1)         print 'Hello, I am xpleaf.'     一般情况下,如果想要计算该程序的执行时间(因为有可能要对该接口进行某些性能上的测试),就需要把以上接口修改为下面这样,同时执行一下: 1 2