前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >装饰器探析

装饰器探析

原创
作者头像
Yerik
修改2021-10-25 10:28:34
2800
修改2021-10-25 10:28:34
举报
文章被收录于专栏:烹饪一朵云

python的学习过程中,如果我们想进阶的话,装饰器这个概念无论如何都是绕不开的,初学者甚至还嫌弃上了,这玩意有什么用呢?看着概念又费解,还用的不畅快,不如不学,正所谓技多不压身,学了顶多掉头发不是嘛,没其他坏处O(∩_∩)O而且,这个是从初学者向高阶晋级的一道坎

装饰器概念

python中的装饰器是为对象提供一些额外的功能,这些对象可以是类、方法、也可以是函数等等。装饰器本身就是一个闭包的一种应用,而闭包就是在函数中再嵌套一个函数,并且可以引用外包函数的变量。装饰器就是用于扩展原来函数的一种函数,类似原函数的插件,按照现在的说法应该是原函数plus,这个插件函数的返回值比较特殊,也是一个函数,其他的也就在使用上,需要在相对应的函数头上加@demo就行了

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

理解

什么叫装饰,就是装点、提供一些额外的点缀,前面说的装饰器的相关概念呢,确实有点绕口啥的,我这里举个简单的例子,来辅助一下理解,比如说女朋友跟你出来约会,本来人家就天生丽质了,还要精心打扮一下,头上戴个小发夹,手上小手提,还要有个小手环,还要再问一下:“宝儿,今天我有什么不一样?”

兄弟们这个回答有啥好犹豫的:“没啥不一样啊,是更美了好像?”

这就是装饰器的本质,不改变该对象的基本特性,女票原本什么样还是怎么样,不以外在形象为转移,关键是变美了,这是新特性就是装饰器带来的,比如说女票的小发夹带着更俏皮了,小手环袋子就温婉了一点,不是嘛?

更进一步

在使用装饰器的过程中,我们可以更进一步的体会到面向对象编程的开放封闭原则的应用

开放封闭原则主要体现在两个方面:

  • 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
  • 对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类尽任何修改。

为什么要用装饰器

假设你写了几个函数,有一天老板心血来潮说,你把每个函数的运行时长(结束时间-开始时间)统计下,作为一个python101练习生的你可能会这样写

原始函数

代码语言:txt
复制
def func_a():
    print("hello")
    
def func_b():
    print("naug")

if __name__ == '__main__':
    func_a()
    func_b()

计算运行时间

作为python101练习生的你,大概率会这样写

代码语言:txt
复制
import time
def func_a():
    start = time.time()
    print("love")
    time.sleep(0.8)
    end = time.time()
    print("运行时长:%.4f 秒" % (end-start))

def func_b():
    start = time.time()
    print("naug")
    time.sleep(0.8)
    end = time.time()
    print("运行时长:%.4f 秒" % (end-start))

if __name__ == '__main__':
    func_a()
    func_b()
使用装饰器前.png
使用装饰器前.png

上面的代码虽然满足了领导的要求,但是如果你写的函数很多的话,每个函数都这样去添加,会显得代码很臃肿,有很多重复代码。老板看了代码大概率只会摇头和沉默,唉,毕竟是练习生还能要求啥呢,再带带吧

然后边上的一个扫地僧看了下你的代码,给你指了条明路:装饰器

函数装饰器

装饰器可以写成函数式装饰器,也可以写成一个类装饰器,先从简单的函数装饰器开始学习。

再强调一下,python装饰器本质上就是一个函数,是闭包在python中的一种实现,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象。

runtime函数就是一个装饰器了,它对原函数做了包装并返回了另外一个函数,额外添加了一些功能。在函数上方使用@语法糖就可以调用这个装饰器了

代码语言:txt
复制
import time
def runtime(func):
    def wrapper():
        start = time.time()
        f = func()     # 原函数
        end = time.time()
        print("运行时长:%.4f 秒" % (end-start))
        return f
    return wrapper


@runtime
def func_a():
    print("love")
    time.sleep(0.5)

@runtime
def func_b():
    print("naug")
    time.sleep(0.8)

if __name__ == '__main__':
    func_a()
    func_b()
函数装饰器.png
函数装饰器.png

感受到了吗,当我们成功封装了这个装饰器,也就意味着我们构建完成一个函数运行通用计时器runtime,通过这个计时器,我们在需要测算运行性能的函数都可以通过@runtime的形式来进行挂载使用,从而完成对相对应的函数的运行时间测算,一次开发,多次使用,降低重复造轮子的成本,总之一个字!绝

函数带参数

事实上这个runtime并不强壮,如果函数里面带有参数,那就不管用了,并且函数的参数是不固定的,这时候就需要用到*args,**kwargs这个组合了

代码语言:txt
复制
import time

def runtime(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        f = func(*args, **kwargs)     # 原函数
        end = time.time()
        print("运行时长:%.4f 秒" % (end-start))
        return f
    return wrapper

@runtime
def func_a(girl):
    print("love "+girl)
    time.sleep(0.5)
    
@runtime
def func_b(b, c="xx"):
    print("hi "+b+c)
    time.sleep(0.8)

if __name__ == '__main__':
    func_a("naug")
    func_b("world ", c="new start")
函数带参数.png
函数带参数.png

类装饰器

说到__call__,那就需要再补充一个个概念,可调用对象(callable),我们平时自定义的函数、内置函数和类都属于可调用对象,凡是可以把一对括号()应用到某个对象身上都可称之为可调用对象,判断对象是否为可调用对象可以用函数callable。如果在类中实现了__call__方法,那么实例对象也将成为一个可调用对象

还是原来那个计时器的例子,我们将这个方法封装成一个对象,再来体会一下类装饰器的特点

代码语言:txt
复制
import time

class runtime(object):
    def __init__(self, func):
        self.func = func 
        
    def __call__(self, *args, **kwargs):
        start = time.time()
        f = self.func(*args, **kwargs)     # 原函数
        end = time.time()
        print("运行时长:%.4f 秒" % (end-start))
        return f
        
@runtime
def func_a(girl):
    print("love "+girl)
    time.sleep(0.5)

@runtime
def func_b(b, c="xx"):
    print("hi "+b+c)
    time.sleep(0.8)

if __name__ == '__main__':
    func_a("naug")
    func_b("world ", c="new start")
类装饰器.png
类装饰器.png

装饰器带参数

冬天到了,老板说运行的速度先不要太快,让客户先加钱,然后再以正常的速度显示,那么现在的需求是让每个函数的运行时间加50%,该如何实现呢?

这就到了装饰器的高级语法,装饰器也需要带上参数了

函数装饰器
代码语言:txt
复制
import time
def runtime(slowly=1):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            start = time.time()
            f = func(*args, **kwargs)     # 原函数
            end = time.time()
            t = end-start
            time.sleep((slowly-1)*t)  # 延迟效果
            new_end = time.time()
            print("运行时长:%.4f 秒" % (new_end-start))
            return f
        return inner_wrapper
    return wrapper

@runtime(1.5)
def func_a(girl):
    print("love "+girl)
    time.sleep(0.5)

@runtime(1.5)
def func_b(b, c="xx"):
    print("hi "+b+c)
    time.sleep(0.8)

if __name__ == '__main__':
    func_a("naug")
    func_b("world ", c="new start")
类装饰器带参数.png
类装饰器带参数.png
类装饰器
代码语言:txt
复制
import time
class runtime(object):
    def __init__(self, slowly=1):
        self.slowly = slowly
        
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            start = time.time()
            f = func(*args, **kwargs)     # 原函数
            end = time.time()
            t = end-start
            time.sleep((self.slowly-1)*t)  # 延迟效果
            new_end = time.time()
            print("运行时长:%.4f 秒" % (new_end-start))
            return f
        return wrapper

@runtime(1.5)
def func_a(girl):
    print("love "+girl)
    time.sleep(0.5)
    
@runtime(1.5)
def func_b(b, c="xx"):
    print("hi "+b+c)
    time.sleep(0.8)

if __name__ == '__main__':
    func_a("naug")
    func_b("world ", c="new start")
类装饰器参数.png
类装饰器参数.png

装饰器应用场景

上面的例子主要是一个很简单的应用场景,如果你用过locust,设置权重会用到@task(1),如果你用过pytest框架,使用fixture功能的时候经常会用到@pytest.fixture(scope="function"),也被大量使用于FlaskDjango等web框架中,检查是否被授权去使用一个web应用的端点(endpoint)。如@login_required也可以自己写个装饰器添加日志

装饰器执行机制分析

以前面的runtime为例,我们来对装饰器的运行机制来进行解析,

代码语言:txt
复制
def runtime(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        f = func(*args, **kwargs)     # 原函数
        end = time.time()
        print("运行时长:%.4f 秒" % (end-start))
        return f
    return wrapper
  1. 程序开始运行,从上往下解释,读到def runtime(func):的时候,发现这是个一等公民函数,于是把函数体加载到内存里,之后进行下一个函数的解析
  2. 读到@runtime的时候,程序被@这个语法糖吸引住了,知道这是个装饰器,按规矩要立即执行的,于是程序开始运行@后面那个名字runtime所定义的函数。
  3. 程序返回到runtime函数,开始执行装饰器的语法规则。规则是:被装饰的函数的名字会被当作参数传递给装饰函数。装饰函数执行它自己内部的代码后,会将它的返回值赋值给被装饰的函数。原来的func_a函数被当做参数传递给了slowly,而func_a这个函数名之后会指向wrapper函数。
装饰器执行机制分析.png
装饰器执行机制分析.png

注意:

  • @runtime@runtime()有区别,没有括号时,runtime函数依然会被执行,这和传统的用括号才能调用函数不同,需要注意一下
  • func_a这个函数名(而不是func_a()这样被调用后)当做参数传递给装饰函数runtime,也就是:func = func_a@runtime等于runtime(func_a),实际上传递了func_a的函数体,而不是执行func_a后的返回值。
  • runtime函数return的是wrapper这个函数名,而不是wrapper()这样被调用后的返回值。
  1. 程序开始执行runtime函数内部的内容,一开始它又碰到了一个函数wrapperwrapper函数定义块被程序观察到后不会立刻执行,而是读入内存中(这是默认规则)。
  2. 再往下,碰到return wrapper,返回值是个函数名,并且这个函数名会被赋值给func_a这个被装饰的函数,也就是func_a = wrapper。根据前面的知识,我们知道,此时func_a函数被新的函数wrapper覆盖了(实际上是func_a这个函数名更改成指向wrapper这个函数名指向的函数体内存地址,func_a不再指向它原来的函数体的内存地址),再往后调用func_a的时候将执行wrapper函数内的代码,而不是先前的函数体。那么先前的函数体去哪了?还记得我们将func_a当做参数传递给runtime这个形参么?func这个变量保存了老的函数在内存中的地址,通过它就可以执行老的函数体,你能在wrapper函数里看到f = func()这句代码,它就是这么干的!
  3. 当业务需要,依然通过func_a()的方式调用func_a函数时,执行的就不再是旧的func_a函数的代码,而是wrapper函数的代码。在本例中,它首先会记录开始时间戳,当然了根据业务需要,显然你可以换成任意的代码,然后,它会执行func函数并将返回值赋值给变量f,这个func函数就是旧的func_a函数,执行完成之后记录结束时间戳,再打印时间间隔,最后返回f这个变量。我们在业务需要的代码上可以用f = func_a()的方式接受f的值。
  4. 以上流程走完后,你应该看出来了,这个过程中没有对原来函数(方法)和接口调用方式做任何修改,也没有对基础平台部原有的代码做内部修改,仅仅是添加了一个装饰函数,就实现了我们的需求,在函数调用开始时间计算,调用后写入日志。这就是装饰器的最大作用。

那么为什么我们要搞一个runtime函数一个wrapper函数这么复杂呢?一层函数不行吗?

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

代码语言:txt
复制
def runtime(func):
    start = time.time()
    f = func()     # 原函数
    end = time.time()
    print("运行时长:%.4f 秒" % (end-start))
    return f
    

@runtime
def func_a():
    print("love naug")
    time.sleep(0.5)

if __name__ == '__main__':
    func_a()
错误使用.png
错误使用.png

看见了吗?我们只是定义好了装饰器,业务代码还没有调用runtime函数呢,程序就把工作全做了。这就是为什么要封装一层函数的原因。

更进一步

装饰器执行顺序是否有规定呢?到底是按照我们添加语法糖的顺序来加载还是倒序的输出呢?这个是应用了队列结构还是栈的结构呢?不妨来进行下一步的实验

代码语言:txt
复制
import time

def runtime(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        f = func(*args, **kwargs)     # 原函数
        time.sleep(0.5)
        end = time.time()
        print("运行时长:forever")
        return f
    return wrapper

def dating(func):
    def wrapper(*args, **kwargs):
        f = func(*args, **kwargs)     # 原函数
        print("nice time!")
        return f
    return wrapper

@dating
@runtime
def func_a(girl):
    print("love "+girl)


@runtime
@dating
def func_b(girl):
    print("love "+girl)


if __name__ == '__main__':
    func_a('naug')
    func_b('naug')
运行对比.png
运行对比.png

通过这个例子我们知道了函数是可以被多个装饰器装饰的,就像女孩子会带上各种各种漂漂亮亮的装饰品一样,我们这里要关注的是加载语法糖的顺序对执行器的影响,从demo中我们可以看到,func_afunc_b都是优先执行自己方法里面的逻辑,之后再执行装饰器的逻辑,越靠近def的越先执行

参考资料

  1. Decorators I: Introduction to Python Decorators
  2. Python Decorators II: Decorator Arguments

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 装饰器概念
    • 理解
      • 更进一步
        • 为什么要用装饰器
      • 函数装饰器
        • 函数带参数
      • 类装饰器
        • 装饰器带参数
      • 装饰器应用场景
      • 装饰器执行机制分析
        • 更进一步
        • 参考资料
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档