Python - 装饰器

一、装饰器的基本使用

在不改变函数源代码的前提下,给函数添加新的功能,这时就需要用到“装饰器”。

0.开放封闭原则

写代码要遵循开放封闭原则,它规定“已经实现的功能代码不允许被修改,但可以被扩展。”——封闭即是已经实现的功能代码块,开放是对扩展开放。

1.装饰器本质

装饰器本质上就是一个闭包 ,但是是一个特殊的闭包。当闭包函数有且只有一个参数,并且该参数必须是函数类型,此时闭包才称为“装饰器”。

2.装饰器的功能特点

1)给已有函数增加额外的功能;

2)但是不修改已有函数的源代码;

3)不修改已有函数的调用方式;

3.装饰器定义格式

# 定义装饰器

def decorator(func):

    def wrapper(*args, **kwargs):

        要添加的额外功能

        return func(*args, **kwargs)

    return wrapper

其中:

1)func:是一个函数(函数作为参数);

2)wrapper:内层闭包函数,调用了func;

3)return wrapper:外层函数decorator返回值为内层函数wrapper;

4.装饰器的使用

先定义要装饰的函数:

def print_info():

    print('打印的内容……')

为函数print_info添加功能:打印前提示,打印后提示

# 定义装饰器

def decorator(func):

    def wrapper(*args, **kwargs):

        print('***我要开始打印正文了***')

        func()

        print('***全文已打印完毕***')

        return func(*args, **kwargs)

    return wrapper


# 使用装饰器:将print_info函数作为装饰器的参数

print_info = decorator(print_info)

print_info()

>>> 运行结果

***我要开始打印正文了***

打印的内容……

***全文已打印完毕***

可以看出,1)为函数print_info添加功能成功;2)没有修改原函数;3)原函数调用方式也没变;

装饰器内部运行原理:

说明:

1)print_info = decorator(print_info),此处只装饰函数,并且把装饰器的内层函数返回,并赋值给print_info,此时print_info实际已经是wrapper函数;

2)print_info(),此处调用函数时,才正式开始执行已经添加功能的函数(wrapper函数);

3)wrapper函数中的func()调用的是原函数,因为闭包函数能够保存它使用的外层函数的变量;

5.注意事项

由上面可以看出装饰原函数后,原函数print_info的函数名已经变成wrapper(print_info.__name__='wrapper')。

为了不改变原函数的函数名,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。Python内置的装饰器functools.wraps能够实现。因此严格意义上装饰器的定义应为:

import functools

def decorator(func):

    @functools.wraps(func)

    def wrapper(*args, **kwargs):

        要添加的额外功能

        return func(*args, **kwargs)

    return wrapper

6.装饰器的装饰方式

方式一:直接调用

func = decorator(func)  # 直接调用装饰器函数,传入函数名,并赋值给函数名

但每次都写调用-赋值语句比较麻烦,于是Python提供了比较简单的第二种“语法糖”书写格式

方式二:语法糖--@装饰器名字

@decorator              # 在函数定义语句上面直接“@装饰器名字”

def func():

    ...

语法糖装饰是常用的装饰器装饰方式,“@decorator”就等价于func = decorator(func)。

装饰器在模块进行加载时就会立即执行装饰过程。

7.装饰器使用场景

1)引入日志;

2)函数执行时间统计;

3)执行函数前预备处理;

4)执行函数后清理功能;

5)权限校验等场景;

6)缓存;

8.应用举例

统计函数的执行时间:

# 统计函数执行时间的装饰器

import functools

import time

def execute_time(func):

    @functools.wraps(func)

    def wrapper(*args, **kwargs):

        # 程序开始时间点

        start = time.time()

        # 执行函数

        result = func(*args, **kwargs)

        # 程序结束时间点

        end = time.time()

        # 计算执行时间

        execution_time = end - start

        print('{}执行时间:{}s'.format(func.__name__, execution_time))

        # 返回函数结果

        return result

    return wrapper


# 定义一个简单的能够指定循环次数的函数,并装饰

@execute_time

def loop_count(n):

    # 循环打印

    for i in range(n):

        print('第{}次...'.format(i+1)) 



# 看看循环1000000次需要多长时间?

loop_count(1000000)

>>> 运行结果:

第1次...

第2次...

...

第1000000次...

loop_count执行时间:6.355999231338501s

二、通用装饰器

要装饰的函数可能不需要传参,也可能需要传参;然后传多少个参数,是位置传参还是关键字传参,有没有返回值,这些都不能事先知道。也就是说,不可能针对性地去定义装饰器。于是,就需要定义通用的装饰器。不论被装饰的函数是否需要传参,传几个参数,怎么传参,有无返回值,通用装饰器都能使用。

通用装饰器的定义格式:

def decorator(func):

    def wrapper(*args, **kwargs):

        func执行前要添加的额外功能

        result = func(*args, **args)

        func执行后要添加的额外功能

        return result

    return wrapper

说明:

1)内层闭包函数使用*args、**kwargs接收参数,能够应对所有形式的不定长传参;

2)result为被装饰函数的返回值,在闭包函数中return返回;

解决了传参和返回值的问题,实现了通用性。

三、多个装饰器

多个装饰器同时装饰一个函数

# 装饰器1:将原函数输入内容用“()”括起

def add_little_sign(func):

    def wrapper1(*args, **kwargs):

        origin_info = func(*args, **kwargs)

        return '(' + origin_info + ')'

    return wrapper

# 装饰器2:将原函数输入内容用“{}”括起

def add_big_sign(func):

    def wrapper2(*args, **kwargs):

        origin_info = func(*args, **kwargs)

        return '{' + origin_info + '}'

    return wrapper

# 被装饰函数:打印info

@add_big_sign      # 外层

@add_little_sign      # 内层

def print_info(info):

    return info

# 调用装饰后的函数

print(print_info('Hello World!'))

>>> 运行结果:

{(Hello World!)}

可以看到,先实现了add_little_sign的功能,然后实现add_big_sign的功能。

1)上面语法糖的装饰过程等价于:

print_info = add_little_sign(print_info)

print_info = add_big_sign(print_info)

装饰过程:

原print_info函数 --> 执行“print_info = add_little_sign(print_info)” ,使用add_little_sign装饰原函数print_ifo --> print_info = wrapper1 --> 执行“print_info = add_big_sign(print_info)” ,使用add_big_sign装饰函数print_ifo(此时函数print_info已是函数wrapper1) --> print_info = wrapper2

当装饰完毕后,原print_info已经变身成了wrapper2,并且此时wrapper2中储存着传入的wrapper1。那么再调用此时的print_info,执行过程为:调用装饰后的print_info --> 调用wrapper2 --> wrapper2内部调用wrapper1 --> 执行wrapper1 --> 装饰“()” --> 再装饰“{}” --> 完毕。

2)使用多个装饰器时功能特性

(1)一般使用语法糖方式装饰;

(2)会先用内层的装饰,再用外层的装饰;

多个装饰器同时使用时,相当于先将内层的装饰器包裹住原函数,再在外面用外层的装饰器包裹。

四、带参数的装饰器

有时装饰器也需要携带参数,以实现对要添加功能的处理。

错误的装饰器携带参数的格式:

def decorator(func, a):

    def wrapper(*args, **kwargs):

        要添加的额外功能

        (对参数a进行操作)

        return func(*args, **kwargs)

    return wrapper

@decorator(2)      # 假如将参数a设置为2

def func():

    代码块...


>>> 运行结果:

TypeError: 'int' object is not callable   

因为装饰器只能接收一个参数,并且该参数必须是可调用的函数对象。

正确的装饰器携带参数的格式:

def create_decorator(a):

    def decorator(func):

        def wrapper(*args, **kwargs):

            要添加的额外功能

            (对参数a进行操作)

            return func(*args, **kwargs)

        return wrapper

    return decorator

@create_decorator(2)      # 假如将参数a设置为2

def func():

    代码块...


>>> 运行结果:

会正常运行,不会再抛出异常   

上述代码块,定义了一个返回装饰器的闭包,通过闭包能够存储外层函数变量的特性保存住外层函数的参数,然后在装饰器中进行处理。

装饰过程等价于:

decorator = create_decorator(2)    # 传入 参数2,被储存至返回的装饰器内

func = decorator(func)      # 这步开始才是真正的装饰器装饰过程

...

五、类装饰器

装饰器还有一种特殊的定义方法,就是通过定义一个类来定义装饰器。

class Decorator(object):

    def __init__(self, func):

        # 初始化操作在此完成

        self.__func = func

    # 实现__call__方法,表示对象是一个可调用对象,可以像调用函数一样进行调用。

    # 重写类的可调用魔法方法

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

        # 添加装饰功能

        要添加的额外功能

        self.__func()

@Decorator

def comment():

    print("发表评论")

comment()

说明:

在定义类的初始化方法时,定义一个私有实例属性,此属性是用来接收要装饰的函数对象;然后重写类的__call__方法,使对象被调用时实现原函数功能的基础上增加新功能。(拥有__call__方法能够使对象被调用,“对象()”会自动执行对象的__call__方法。)

上述装饰器装饰过程等价于:

comment = Decorator(comment)    # 传入comment进行实例化对象

comment()      # 此步将执行类的实例方法__call__,从而实现了增加的功能

传入原函数comment,Decorator接收并实例化出一个对象 --> comment = 实例化的对象 --> comment() --> 执行实例对象的__call__方法 --> 完成装饰。

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