装饰器详解

装饰器详解

闭包

要想理解装饰器,首先得弄明白什么是闭包

函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包


def wrapper():

    name = "ivy"



    def inner():

        print(name)

    return inner



g = wrapper()

g()

根据上面的定义,wrapper函数里面定义了inner函数,inner函数里面使用了wrapper中的name变量。wrapper函数被调用后会返回内部的inner函数给g,当g再次被调用时,就会形成闭包。

为什么要使用装饰器

需求: 在不修改调用代码的情况下,计算一段业务代码的耗时


import time

def your_work():

    start = time.time()

    time.sleep(3)  # 模仿业务耗时

    end = time.time()

    print(end - start)





your_work()

这里的time.sleep模拟任务的耗时,从这段代码中可以看到,计时和实际的业务代码写在一个函数中,可读性差,如果从面向对象的角度来看,这段代码违反了单一职责原则。


import time

def your_work():

    time.sleep(3)



def spent_time(func):

    start = time.time()

    func()

    end = time.time()

    print(end - start)





spent_time(your_work)

将上面的代码修改,将计时和业务代码分开,虽然可读性高了,但是调用方式发生了改变,不符合需求。

这个时候,就要使用到装饰器了。

装饰器原理


import time

def wrapper(func):

    print("wrapper....")

    def inner():

        start = time.time()

        func()

        end = time.time()

        print(end - start)



    return inner

def your_work():

    time.sleep(3)

g = wrapper(your_work)

g()

分析:

  • 利用上面的闭包原理,wrapper函数接受一个函数作为参数,当wrapper函数被执行的时候,实际执行print("wrapper....")和返回inner函数,因为执行wrapper函数的时候inner函数没有被执行,所以传进来的参数函数也没有被执行。

  • 在上述代码中,只有当wrapper函数的接受者(inner的接受者)g被调用的时候,inner才会被执行。因为inner里面有且仅有func,也就是传进来的your_work,所以当g被调用的时候,也就是your_work被调用的时候,这时候可以把g看做your_work


import time

def wrapper(func):

    print("wrapper....")

    def inner():

        start = time.time()

        func()

        end = time.time()

        print(end - start)

    return inner

@wrapper

def your_work():

    time.sleep(3)

your_work()

python提供了一种语法糖@,@的作用只是将所装饰的函数(your_work)传入装饰器函数(wrapper)作为参数,并执行装饰器函数。当被装饰的函数(your_work)被调用的时候,实际上是调用的装饰器内部返回的函数(inner), 这样就可以完成不修改调用处代码,又能达到计时目的。

装饰器返回值和装饰器参数

上述的所装饰的函数(your_work)既没有返回值也没有参数,因为被装饰的函数最后会执行装饰器函数所返回的闭包函数(inner), 所以inner所接受的参数和返回的值就是被装饰函数返回的值和接受的参数,因为装饰器最初的目的是为原有的代码增加功能而不修改原来的逻辑,所以函数的返回值和函数的参数应该和被装饰的函数一致。


import time

def wrapper(func):

    def inner(*args, **kwargs):

        start = time.time()

        result = func(*args, **kwargs)

        end = time.time()

        print(end - start)

        return result



    return inner

@wrapper

def do_something(num):

    time.sleep(3)

    return num



在inner里面使用不定长参数来接受任意参数,在调用参数func的函数时候将接受的参数传递给func,在将函数运行的结果保存,最后返回即可。

带参数的装饰器


from flask import Flask

app = Flask(__name__)

@app.route('/index')

def index():

    return 'index page'

if __name__ == '__main__':

    app.run()

熟悉flask 的应该很熟悉这段代码,flask 使用装饰器接受参数来定义路由。

模仿flask写一个接受参数的装饰器


class App:

    def __init__(self):

        self.urls = dict()

    def route(self, url:str):

        def wrapper(func):

            self.urls[url] = func

            return func

        return wrapper

app = App()

@app.route('/index')

def index():

    return 'index page'

在App.route方法中,接受一个字符串作为参数,再在内部将接受的参数func做一个映射关系存入实例属性中。

注意

  • 在app.route对index进行装饰的时候,app.route接受了一个字符串作为参数,主动执行了route方法,返回了wrapper函数,然后@语法在使用route返回的wrapper对index进行了装饰。所以index函数最后传入了wrapper中而不是直接传入route。

类装饰器


import time

class Wrapper:

    def __init__(self, func):

        self.func = func

    def __call__(self, *args, **kwargs):

        start = time.time()

        result = self.func(*args, **kwargs)

        end = time.time()

        print(end - start)

        return result

@Wrapper

def your_work(num):

    time.sleep(3)

    return num

类也可以作为一个装饰器,根据上述原理,装饰器会将your_work传入Wrapper,由于类直接被调用会触发它的init_方法,所以在实例化方法里面接受被装饰器的函数作为实例函数属性保存。此时被装饰的函数可以看到装饰器类的一个实例变量,这个函数被调用的时候(可以看成是类的实例变量被调用,此时会触发该类的call方法),最后在call方法里面写上自己的处理逻辑即可

一个简单的缓存装饰器


import time

class Cache:

    def __init__(self, func):

        self.func = func

        self._value = None

    def __call__(self, *args, **kwargs):

        if self._value is None:

            result = self.func(*args, **kwargs)

            self._value = result

        return self._value

@Cache

def your_work(num):

    time.sleep(3)

    return num

print(your_work(3))

print(your_work(4))

在这个例子中,假设your_work是一个耗时的任务,但是它每次返回的结果都相同,所以可以在它第一次运行完毕之后将它的结果保存起来,下次再次调用它的时候直接将结果返回,这样就避免了重复的运算。

装饰类

装饰器不仅可以装饰函数,也可以装饰类,原理和函数一致,只用将传递的参数函数换成类即可!

多个装饰器


from tornado.httpclient import AsyncHTTPClient

from tornado.web import gen

from tornado.ioloop import IOLoop

class Request:

    @classmethod

    @gen.coroutine

    def get_resp(cls, url):

        client = AsyncHTTPClient()

        resp = yield client.fetch(url)

        print(resp.body)

if __name__ == '__main__':

    Request.get_resp('https://www.baidu.com')

    IOLoop.instance().start()

熟悉tornado的都应该熟悉这段代码,这是tornado实现一个异步获取网络响应的最基本的写法。

当一个对象被多个装饰器所装饰,那么装饰器的执行顺序是怎样的?


def test2(func):

    def inner():

        print("test2----before")

        func()

        print("test2----after")

    return inner

def test3(func):

    def inner():

        print("test3----before")

        func()

        print("test3----after")

    return inner

@test3

@test2

def test1():

    print("test1---")

if __name__ == '__main__':

    test1()

""" 运行结果

test3----before

test2----before

test1---

test2----after

test3----after

"""

洋葱模型

从上面的运行结果可以看出,当一个对象被多个装饰器所装饰的时候,所有的需要在该对象本身test1运行前而运行的顺序是根据装饰器装饰的顺序从上而下的,而所有需要在被装饰对象test1本身运行后所运行的顺序是装饰器装饰顺序由下而上的。

如果读懂了上面的装饰器原理,那么很容易理解这个例子

我们可以把这个顺序看成一个洋葱模型,先从外面一层层进去,最后从里面一层层出来。

装饰器的基本原则

装饰器的本意是在不修改原来的代码和调用方式的情况下给被装饰的对象增加新的功能,所以我们在书写装饰器的时候如果没有特殊需求的话,尽量不要在装饰器内部去修改被装饰器对象的返回值(如果有返回值),这样可以使用多个装饰器来装饰而不容易发生错误。

装饰器的本质就是一个可接收参数callable的对象去装饰另外一个callable对象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,076评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,658评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,732评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,493评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,591评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,598评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,601评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,348评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,797评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,114评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,278评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,953评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,585评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,202评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,180评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,139评论 2 352