Python核心编程-装饰器

531 阅读8分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

本文同时参与「掘力星计划」,赢取创作大礼包,挑战创作激励金

 装饰器

装饰器(Decorators)是 Python 的一个重要组成部分,在程序开发中经会常用到,用好了装饰器开发效率事半功倍。简单地说:它是修改其他函数功能的函数。有助于让我们的代码更简短,也更Pythonic(Python范儿)。

python装饰器本质上就是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数(函数的引用

装饰器的概括理解:

    实质: 就是一个闭包函数

  参数:要装饰的函数(是函数名并非函数的调用,即不带括号)

  返回:是装饰完的函数(也是函数名而不是函数调用)

  作用:为已经存在的函数在不做任何内部修改的情况下添加额外的功能

  特点:要装饰的函数的内部代码无需任何改动

1. 以一个例子开始

某公司有个员工信息管理系统,主要有添加员工信息,更新员工信息,删除员工信息,查看员工信息等功能,任何人都可以访问进行操作。用了段时间发现这样不行应该做下限制,在每个操作前进行登录校验,只有登录过的人才可操作,原有功能如下:

def add_staff():
    print('添加员工信息')

def del_staff():
    print("删除员工信息")

def upd_staff():
    print("更新员工信息")

def view_staff():
    print("查看员工信息")

于是技术主管把这项任务同时交给了小A和小B2个人。

小A是将要毕业的实习生,他的实现方式是在每个功能函数中分别添加校验逻辑,如下:

def add_staff():
    #登录验证
    print('添加员工信息')

def del_staff():
    #登录验证
    print("删除员工信息")

def upd_staff():
    #登录验证
    print("更新员工信息")

def view_staff():
    #登录验证
    print("查看员工信息")

小B是个有一定工作经验的工程师了,他给出的方案是,定义一个额外的登录校验函数,然后在功能函数中分别调用,如下:

def add_staff():
    check_login()
    print('添加员工信息')

def del_staff():
    check_login()
    print("删除员工信息")

def upd_staff():
    check_login()
    print("更新员工信息")

def view_staff()
    check_login():
    print("查看员工信息")

def check_login():
    #登录验证

任务完成后技术主管分别看了下两个人的完成情况,但并没有发表什么意见,而是又提了个新的需求,要求再加一个权限校验,验证通过后方可操作。于是小A小B回去继续修改,小A依然是在每个功能函数中添加权限验证逻辑,小B依然是定义一个权限验证函数然后分别在每个功能函数中调用。

很显然,小B的实现方式明显要好与小A的,试想如果有几十个这样的方法,按照小A的操作方式岂不是同样的代码要copy几十次,一旦逻辑有变又要修改几十次,维护起来非常麻烦。而小B就好多了,只需要改一下校验函数,然后在功能函数中调用一下就行了,效率大大提高。

但第二轮修改完成后,主管看了依然没有发表任何意见,继续又提了个需求(需求变来变去,作为程序员都懂的),要求本着开放封闭的原则,对已经实现了的功能函数的内部代码不允许修改,但该功能函数的功能还得被扩展。即:

  • 封闭:已经实现的功能代码禁止修改
  • 开放:在不改变内部代码的情况下增加其它功能

这下小A是彻底蒙圈了,根本不知从何入手,而小B也是一脸懵逼,于是小A小B开始各种上网查找各种资料,终于在经过几番的苦苦查询后小B找到了答案 - - 装饰器

最终修改后的代码如下:

def check_login(fun):
    def inner():        
        #登录验证
        fun()
    return inner

@check_login
def add_staff():
    print('添加员工信息')

@check_login
def del_staff():
    print("删除员工信息")

@check_login
def upd_staff():
    print("更新员工信息")

@check_login
def view_staff()
    print("查看员工信息")

在不改变内部逻辑的情况下同时完成了操作前的验证功能,主管看满意的点了点头,小B加薪有望了。。。

3. 装饰器详解

单独以add_staff为例:python解释器会从上到下解释代码,步骤如下:

    1) 定义一个闭包函数check_login,即在将函数check_login加载到内存中

    2)@check_login

    3)定义add_staff函数

从表面上看解释器仅仅会解释这几句代码,因为函数在被调用前其内部代码是不会执行的。但是@check_login这一句代码内部却大有文章,@函数名是python中的一种语法糖。

上例@check_login内部会执行以下操作:

执行check_login函数

    调用check_login函数,并将@check_login下面的函数作为check_login的参数传递给check_login,即 @check_login等价于check_login(add_staff),所以内部就会去执行

def inner()
    #登录验证
    fun() #fun是传进来的参数,此时的fun等价于add_staff
return inner 

#fun是传进来的参数,此时的fun等价于add_staff

#返回的是inner,inner也是一个函数,但返回的并不是函数的调用而是将函数的引用(或者说是函数的地址)返回,其实就是将原来的add_staff函数塞进另外一个函数inner中,并且在该函数中进行调用。

所以以后再想添加员工时,调用的还是原来的逻辑,不同的是每次添加前都会有个登录校验。如此一来即执行了验证,又执行了原来的添加员工功能,也就是说在不改变原有逻辑的情况下实现了必要的扩展功能。

4. 装饰器的装饰顺序

还是先看一个列子

#定义一个函数:完成包裹数据
def makeBold(fn):
    def wrapped():
        return '<b>' + fn() + '</b>'
    return wrapped

#定义函数,完成数据包裹
def makeItalic(fn):
    def wrapped():
        return '<i>' + fn() + '</i>'
    return wrapped

@makeBold
def test1():
    return 'hello world-1'

@makeItalic
def test2():
    return 'hello world-2'

@makeBold
@makeItalic
def test3():
    return 'hello world-3'

#调用函数
print(test1())
print(test2())
print(test3())

#运行结果:

<b>hello world-1</b>
<i>hello world-2</i>
<b><i>hello world-3</i></b>

从上面的例子中我们会发现,test1和test2跟我们上面讲的没有什么不同,都是在调用功能函数前进行一下其它的扩展,而在这个例子中则是在返回一个字符串前,先对字符串进行加粗或斜体的一个包裹。

接下来重点看一下第三个例子,这里我们对test3进行了两次装饰分别是@makeBold和@makeItalic,单从代码的执行顺序看应该是先进行了makeBold装饰然后再进行makeItalic装饰,也就是说先调用makeBold进行包裹然后再拿着返回的结果去调用makeItalic进行< i>包裹,那么最终的结果应该是< i >< b >hello world-3</ b ></ i>。然而从运行结果上看,跟我们想象的恰恰相反,实际上是先进行了makeItalic装饰后进行makeBold装饰。这也就是本节的重点所在。

即:在同时有多个装饰器对同一个函数进行装饰的时候,python解释器会按顺序从上向下进行解释,但在装饰的时候,却是从离函数最近的装饰器开始从下向上进行装饰,这也就是为什么我们想象的跟实际运行的结果不一样的原因。

理解了上面这段话后,我们在回到之前的列子,首先python解释器先解释@makeBold然后再去解释@makeItalic,然后在装饰时先进行makeItalic装饰然后带着装饰后的结果再进行makeBold装饰,因此我们看到的结果是< b>< i>hello world-3</ i></ b>而不是< i>< b>hello world-3</ b></ i>

下面再用代码分步理解一下:

#定义一个函数:完成包裹数据
def makeBold(fn):
    def wrapped():
        return '<b>' + fn() + '</b>'
    return wrapped

#定义函数,完成数据包裹
def makeItalic(fn):
    def wrapped():
        return '<i>' + fn() + '</i>'
    return wrapped


@makeBold
@makeItalic
def test3():
    return 'hello world-3'

#装饰步骤如下:首先对test3进行makeItalic装饰
@makeItalic
def test3():
    return 'hello world-3'

#装饰后的结果可理解为
def test3():
    return '<i>hello world-3</i>'
#如果只有一个装饰器的情况下,到这步也就结束了,但是现在是多个装饰器同时装饰,所以在makeItalic装饰结束后上面还有个makeBold,那么接下来就是:
@makeBold
def test3():
    return '<i>hello world-3</i>'

#所以最终再经过makeBold装饰后结果就是:'<b><i>hello world-3</i></b>'

5. 被装饰的函数有参数和返回值

有时候我们需要装饰的函数会带有一些参数和返回值,这种情况下处理也很简单,就是在定义装饰器时,在闭包函数的内部函数中声明与被装饰的函数有同样的参数即可,如果有返回值,再将被装饰的函数进行返回。

看个例子:

def outer(func):
    def inner(a,b):#这里声明的参数与被装饰的函数的参数一致
        #处理逻辑
        return func()#注意这里是返回函数的调用,如果被装饰的函数没有return,这里可以不用返回
    reutrn inner #这里返回的是函数的引用

@outer
def test(a,b):
    return a+b

6. 装饰器的功能及总结

  1. 引入日志

  2. 函数执行时间统计

  3. 执行函数前预处理

  4. 执行函数后清理功能

  5. 权限校验等场景

  6. 缓存机制

总结:一般情况下为了让装饰器更通用,可以定义一个带有不定长度参数和带有返回值的装饰器。