Python装饰器用途(详解)

一 、装饰器介绍

1.1 为何要用装饰器

软件的设计应该遵循开放封闭原则,即对扩展是开放的,而对修改是封闭的。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着对象一旦设计完成,就可以独立完成其工作,而不要对其源代码进行修改。
软件包含的所有功能的源代码以及调用方式,都应该避免修改,否则一旦改错,则极有可能产生连锁反应,最终导致程序崩溃,而对于上线后的软件,新需求或者变化又层出不穷,我们必须为程序提供扩展的可能性,这就用到了装饰器。

1.2 什么是装饰器

’装饰’代指为被装饰对象添加新的功能,’器’代指器具/工具,装饰器与被装饰的对象均可以是任意可调用对象。概括地讲,装饰器的作用就是在不修改被装饰对象源代码和调用方式的前提下为被装饰对象添加额外的功能。装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等应用场景,装饰器是解决这类问题的绝佳设计,有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
提示:可调用对象有函数,方法或者类,此处我们单以本章主题函数为例,来介绍函数装饰器,并且被装饰的对象也是函数。

二 、装饰器的实现

函数装饰器分为:无参装饰器和有参装饰两种,二者的实现原理一样,都是’函数嵌套+闭包+函数对象’的组合使用的产物。

# 偷梁换柱之后
#func(被装饰函数)的参数什么样子,wrapper的参数就应该什么样子
# func的返回值什么样子,wrapper的返回值就应该什么样子
# func的属性什么样子,wrapper的属性就应该什么样子==》from functools import wraps

2.1 无参装饰器的实现

如果想为下述函数添加统计其执行时间的功能

import time
def index():
    time.sleep(3)
    print('Welcome to the index page’)
    return 200

index() #函数执行

遵循不修改被装饰对象源代码的原则,我们想到的解决方法可能是这样

start_time=time.time()
index() #函数执行
stop_time=time.time()
print('run time is %s' %(stop_time-start_time))

考虑到还有可能要统计其他函数的执行时间,于是我们将其做成一个单独的工具,函数体需要外部传入被装饰的函数从而进行调用,我们可以使用参数的形式传入

def wrapper(func): # 通过参数接收外部的值
    start_time=time.time()
    res=func()
    stop_time=time.time()
    print('run time is %s' %(stop_time-start_time))
    return res

但之后函数的调用方式都需要统一改成

wrapper(index)
wrapper(其他函数)

这便违反了不能修改被装饰对象调用方式的原则,于是我们换一种为函数体传值的方式,即将值包给函数,如下

def timer(func):
    def wrapper(): # 引用外部作用域的变量func
        start_time=time.time()
        res=func()
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper

这样我们便可以在不修改被装饰函数源代码和调用方式的前提下为其加上统计时间的功能,只不过需要事先执行一次timer将被装饰的函数传入,返回一个闭包函数wrapper重新赋值给变量名 /函数名index,如下

index=timer(index)  #得到index=wrapper,wrapper携带对外作用域的引用:
                     func=原始的index
index() # 执行的是wrapper(),在wrapper的函数体内再执行最原始的index

至此我们便实现了一个无参装饰器timer,可以在不修改被装饰对象index源代码和调用方式的前提下为其加上新功能。但我们忽略了若被装饰的函数是一个有参函数,便会抛出异常

def home(name):
    time.sleep(5)
    print('Welcome to the home page',name)

home=timer(home)
home('egon')
#抛出异常
TypeError: wrapper() takes 0 positional arguments but 1 was given

之所以会抛出异常,是因为home(‘egon’)调用的其实是wrapper(‘egon’),而函数wrapper没有参数。wrapper函数接收的参数其实是给最原始的func用的,为了能满足被装饰函数参数的所有情况,便用上*args+**kwargs组合,于是修正装饰器timer如下

def timer(func):
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper

此时我们就可以用timer来装饰带参数或不带参数的函数了,但是为了简洁而优雅地使用装饰器,Python提供了专门的装饰器语法来取代index=timer(index)的形式,需要在被装饰对象的正上方单独一行添加@timer,当解释器解释到@timer时就会调用timer函数,且把它正下方的函数名当做实参传入,然后将返回的结果重新赋值给原函数名

@timer # index=timer(index)
def index():
    time.sleep(3)
    print('Welcome to the index page')
    return 200
    
    
@timer # home=timer(home)
          
def home(name):
    time.sleep(5)
    print('Welcome to the home page’,name)

如果我们有多个装饰器,可以叠加多个

@deco3
@deco2
@deco1
def index():
    pass

叠加多个装饰器也无特殊之处,上述代码语义如下:

index=deco3(deco2(deco1(index)))

2. 2 from functools import wraps的用途

可以使用help(函数名)来查看函数的文档注释,本质就是查看函数的doc属性,但对于被装饰之后的函数,查看文档注释

@timer
def home(name):
    '''
    home page function
    :param name: str
    :return: None
    '''
    time.sleep(5)
    print('Welcome to the home page',name)

print(help(home))
'''
打印结果:

Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

None

在被装饰之后home=wrapper,查看home.name也可以发现home的函数名确实变成了wrapper,但想要保留原函数的文档和函数名属性,需要修正装饰器

def timer(func):
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    wrapper.__doc__=func.__doc__
    wrapper.__name__=func.__name__
    return wrapper

按照上述方式来实现保留原函数属性过于麻烦,functools模块下提供一个装饰器wraps专门用来帮我们实现这件事,用法如下

from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args,**kwargs):
        start_time=time.time()
        res=func(*args,**kwargs)
        stop_time=time.time()
        print('run time is %s' %(stop_time-start_time))
        return res
    return wrapper

2.3 有参装饰器的实现

了解无参装饰器的实现原理后,我们可以再实现一个用来为被装饰对象添加认证功能的装饰器,实现的基本形式如下

# 山炮玩法:
# def auth(func,db_type):
#     def wrapper(*args, **kwargs):
#         name=input('your name>>>: ').strip()
#         pwd=input('your password>>>: ').strip()
#
#         if db_type == 'file':
#             print('基于文件的验证')
#             if name == 'egon' and pwd == '123':
#                 res = func(*args, **kwargs)
#                 return res
#             else:
#                 print('user or password error')
#         elif db_type == 'mysql':
#             print('基于mysql的验证')
#         elif db_type == 'ldap':
#             print('基于ldap的验证')
#         else:
#             print('不支持该db_type')
#
#     return wrapper
#
# # @auth  # 账号密码的来源是文件
# def index(x,y):
#     print('index->>%s:%s' %(x,y))
#
# # @auth # 账号密码的来源是数据库
# def home(name):
#     print('home->>%s' %name)
#
# # @auth # 账号密码的来源是ldap
# def transfer():
#     print('transfer')
#
#
# index=auth(index,'file')   # 不用语法糖,自己手动传参
# home=auth(home,'mysql')
# transfer=auth(transfer,'ldap')
#
# # index(1,2)
# # home('egon')
# # transfer()


# 山炮二
# def auth(db_type):
#     def deco(func):
#         def wrapper(*args, **kwargs):
#             name=input('your name>>>: ').strip()
#             pwd=input('your password>>>: ').strip()
#
#             if db_type == 'file':
#                 print('基于文件的验证')
#                 if name == 'egon' and pwd == '123':
#                     res = func(*args, **kwargs)
#                     return res
#                 else:
#                     print('user or password error')
#             elif db_type == 'mysql':
#                 print('基于mysql的验证')
#             elif db_type == 'ldap':
#                 print('基于ldap的验证')
#             else:
#                 print('不支持该db_type')
#
#         return wrapper
#     return deco
#
# deco=auth(db_type='file')
# @deco # 账号密码的来源是文件
# def index(x,y):
#     print('index->>%s:%s' %(x,y))
#
# deco=auth(db_type='mysql')
# @deco # 账号密码的来源是数据库
# def home(name):
#     print('home->>%s' %name)
#
# deco=auth(db_type='ldap')
# @deco # 账号密码的来源是ldap
# def transfer():
#     print('transfer')
#
#
# index(1,2)
# home('egon')
# transfer()



# 语法糖
# def auth(db_type):
#     def deco(func):
#         def wrapper(*args, **kwargs):
#             name = input('your name>>>: ').strip()
#             pwd = input('your password>>>: ').strip()
#
#             if db_type == 'file':
#                 print('基于文件的验证')
#                 if name == 'egon' and pwd == '123':
#                     res = func(*args, **kwargs)  # index(1,2)
#                     return res
#                 else:
#                     print('user or password error')
#             elif db_type == 'mysql':
#                 print('基于mysql的验证')
#             elif db_type == 'ldap':
#                 print('基于ldap的验证')
#             else:
#                 print('不支持该db_type')
#         return wrapper
#     return deco
#
#
# @auth(db_type='file')  # @deco # index=deco(index) # index=wrapper
# def index(x, y):
#     print('index->>%s:%s' % (x, y))
#
# @auth(db_type='mysql')  # @deco # home=deco(home) # home=wrapper
# def home(name):
#     print('home->>%s' % name)
#
#
# @auth(db_type='ldap')  # 账号密码的来源是ldap
# def transfer():
#     print('transfer')

# index(1, 2)
# home('egon')
# transfer()




# 有参装饰器模板

# def 有参装饰器(x,y,z):
#     def outter(func):
#         def wrapper(*args, **kwargs):
#             res = func(*args, **kwargs)
#             return res
#         return wrapper
#     return outter
#
# @有参装饰器(1,y=2,z=3)
# def 被装饰对象():
#     pass

函数wrapper需要一个driver参数,而函数deco与wrapper的参数都有其特定的功能,不能用来接受其他类别的参数,可以在deco的外部再包一层函数auth,用来专门接受额外的参数,这样便保证了在auth函数内无论多少层都可以引用到

def auth(driver):
    def deco(func):
        ……
    return deco

此时我们就实现了一个有参装饰器,使用方式如下
先调用auth_type(driver='file'),得到@deco,deco是一个闭包函数,
包含了对外部作用域名字driver的引用,@deco的语法意义与无参装饰器一样

@auth(driver='file') 
def index():     
    pass
@auth(driver='mysql') 
def home():
    pass  

2.4 多个装饰器分析

# 一、叠加多个装饰器的加载、运行分析(了解***)

def deco1(func1): # func1 = wrapper2的内存地址
    def wrapper1(*args,**kwargs):
        print('正在运行===>deco1.wrapper1')
        res1=func1(*args,**kwargs)
        return res1
    return wrapper1

def deco2(func2): # func2 = wrapper3的内存地址
    def wrapper2(*args,**kwargs):
        print('正在运行===>deco2.wrapper2')
        res2=func2(*args,**kwargs)
        return res2
    return wrapper2
    
def deco3(x):
    def outter3(func3): # func3=被装饰对象index函数的内存地址
        def wrapper3(*args,**kwargs):
            print('正在运行===>deco3.outter3.wrapper3')
            res3=func3(*args,**kwargs)
            return res3
        return wrapper3
    return outter3


# 加载顺序自下而上(了解)
@deco1      # ===>===>index=deco1(wrapper2的内存地址)     ===> index=wrapper1的内存地址
@deco2      # ===> index=deco2(wrapper3的内存地址)        ===> index=wrapper2的内存地址
@deco3(111) # ===>@outter3===> index=outter3(index)      ===> index=wrapper3的内存地址
def index(x,y):
    print('from index %s:%s' %(x,y))

# 执行顺序自上而下的,即wraper1-》wrapper2-》wrapper3
index(1,2) # wrapper1(1,2)

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值