python自学——装饰器

  • Post author:
  • Post category:python




开放封闭原则和装饰器的应用

  • 已经封装好的代码,可以去拓展,但是如果去修改其内部的代码,是不允许的
  • 装饰器的作用:在不更改原功能函数内部代码,并且不更改调用方法的情况下,为原函数添加新的功能
  • 装饰器的应用场景:
    • 登陆验证
    • 函数运行时间统计
    • 执行函数之前做准备工作
    • 执行函数之后的清理功能



装饰器的演变



初始版本

需求:一个加法函数,想增强他的功能,能够输出比哪调用过一集调用的参数信息

简单的实现,代码如下:

def add(x,y):
    #增加他的输出功能
    print("call add,x+y")#日志输出到控制台
    return x+y
k = add(4,5)
print(k)
  • 以上代码是完成了需求,但是有以下缺点:
  • 打印语句的耦合性太好,他和函数紧紧的粘合在了一起
  • 加法属于业务功能,而输出信息的功能属于非业务功能,不应该放在业务函数中。属于侵入式代码



改进第一步,通过高阶函数来实现

def add(x,y):
    return x+y

def logger(fn):#将函数名传入另外一个函数中
    print("begin")
    x = fn(4,5)
    print("end")
    return x

print(logger(add))
===========run_result==========
begin
end
9
  • 以上代码虽然做到了业务分离,但是fn函数调用传参是个问题,参数被写死了
  • 如何解决传参问题呢?代码如下:
def add1(x,y):
    return  x+y

def add2(x,y,z):
    return x+y+z

def add3(x,y,*args,z):
    return x+y+z

def logger(fn,*args,**kwargs):
    print("before")
    ret = fn(*args,**kwargs)
    print("after")
    return ret

print("add1:",logger(add1,4,5),end=" ")
print("=========分割线=============")
print("add2:",logger(add2,4,5,6),end=" ")
#add2需要传三个参数,如果add1,就需要传2个,否则报错

print("=========分割线=============")
print("add3:",logger(add3,4,5,6,z=7),end=" ")

==============run_result============
before
after
add1: 9 =========分割线=============
before
after
add2: 15 =========分割线=============
before
after
add3: 16 



再次优化,柯里化

  • 柯里化,嵌套函数,外层函数返回内层函数的函数对象
def add1(x,y):
    return  x+y

def add2(x,y,z):
    return x+y+z

def add3(x,y,*args,z):
    return x+y+z

def logger(fn):
    def logger_1(*args,**kwargs):
        print("before")
        ret = fn(*args,**kwargs)
        print("after")
        return ret
    return logger_1


print(logger(add1)(4,5))
print("=========分割线=============")
print(logger(add2)(4,5,6))
print("=========分割线=============")
print(logger(add3)(4,5,6,z=7))

============run_result===============
before
after
9
=========分割线=============
before
after
15
=========分割线=============
before
after
16



再次优化,装饰器

def logger(fn):
    def logger_1(*args,**kwargs):
        print("before")
        ret = fn(*args,**kwargs)
        print("after")
        return ret
    return logger_1

@logger
def add1(x,y):
    return  x+y

@logger
def add2(x,y,z):
    return x+y+z

@logger #@logger等价于 add3=logger(add3)其接收的对象是logger_1
def add3(x,y,*args,z):
    return x+y+z


print(add1(4,5))
print("=========分割线=============")
print(add2(4,5,6))
print("=========分割线=============")
print(add3(4,5,6,z=7))

============run_result============
before
after
9
=========分割线=============
before
after
15
=========分割线=============
before
after
16



装饰器(无参)

  • 装饰器(无参)

    1.他是一个函数

    2.函数作为他的形参

    3.返回值也是一个函数

    4.可以使用@functionname方式,简化调用
  • 装饰器和高阶函数之间的关系

    1.装饰器是高阶函数

    2.但是装饰器是对传入函数功能的装饰(函数增强)
  • 以下是优化后的装饰器的代码
import datetime
import time

def logger(fn):
    def logger_1(*args,**kwargs):
        #befpre 功能增强
        print("args={},kwargs={}".format(args,kwargs))
        start = datetime.datetime.now()
        ret = fn(*args,**kwargs)
        #after 功能增强
        duration = datetime.datetime.now()-start
        print("function{}took{}s".format(fn.__name__,duration.total_seconds()))
        return ret
    return logger_1

@logger   #等价于:add=logger(add)
def add(x,y):
    print("************call add***********")
    time.sleep(2)
    return x+y

print(add(4,y=100))
print(add(5,1000))

=============run_result============
args=(4,),kwargs={'y': 100}
************call add***********
functionaddtook2.005193s
104
args=(5, 1000),kwargs={}
************call add***********
functionaddtook2.003411s
1005



装饰器的作用

  • 功能增强,避免侵入式代码

    在这里插入图片描述



文档字符串

  • python是文档字符串Documentation Strings
  • 在函数语句的第一行,切习惯是多行文本,所以多使用三引号
  • 惯例是首字母大写(英文),第一行写概述,空一行,第三行写详细的描述
  • 可以使用特殊属性

    __doc__

    来访问这个文档
  • 例如以下代码:
def add(x,y):
    '''This is a function of addition'''
    return x+y

print("name={}\ndoc={}".format(add.__name__,add.__doc__))
print(help(add))

================run_result============
name=add
doc=This is a function of addition
Help on function add in module __main__:

add(x, y)
    This is a function of addition

None



装饰器的弊端(副作用)

  • 将文件字符串放到装饰器之后,调用函数时原函数的对象的属性被替换了
  • 代码如下:
import datetime

def logger(fn):
    def logger_1(*args,**kwargs):
        '''This is a logger'''
        #befpre 功能增强
        print("args={},kwargs={}".format(args,kwargs))
        start = datetime.datetime.now()
        ret = fn(*args,**kwargs)
        #after 功能增强
        duration = datetime.datetime.now()-start
        print("function:{},took:{}s".format(fn.__name__,duration.total_seconds()))
        return ret
    return logger_1

@logger  #等价于:add=logger(add)
def add(x,y):
    '''This is a function of addition'''
    return x+y

print("name={}\ndoc={}".format(add.__name__,add.__doc__))
print(add(4,y=100))
print(add(5,1000))

=============run_result==============
name=logger_1
doc=This is a logger
args=(4,),kwargs={'y': 100}
function:add,took:3e-06s
104
args=(5, 1000),kwargs={}
function:add,took:1e-06s
1005



解决方式一

  • 使用装饰器,我们的需求是查看被封装(装饰)函数的属性,如何解决?
  • 答:提供一个函数(

    copy_doc_name

    ),被封装(装饰)函数属性copy(赋值)给装饰器函数
  • 调用这个函数必须在loggger函数return前

  • add(4,y=100)

    在不使用装饰器的情况下,等价于:

    logger(add)(4,5)
  • 代码如下:
import datetime

def copy_doc_name(src,dst):
    dst.__name__ = src.__name__
    dst.__doc__ = src.__doc__

def logger(fn):
    def logger_1(*args,**kwargs):
        '''This is a logger'''
        #befpre 功能增强
        print("args={},kwargs={}".format(args,kwargs))
        start = datetime.datetime.now()
        ret = fn(*args,**kwargs)
        #after 功能增强
        duration = datetime.datetime.now()-start
        print("function:{},took:{}s".format(fn.__name__,duration.total_seconds()))
        return ret
    #在这里,fn和logger_1都已经被定义,因此放在这儿最合适
    #c传进来的参数,src=fn、dst=logger_1
    #函数中dsc的name和doc等于src的name和doc,就是将name和doc重新赋值
    #将fn(add)中的名称和doc给到logger_1,因为最终返回的是logger函数
    copy_doc_name(fn,logger_1)
    return logger_1

@logger  #等价于:add=logger(add)
def add(x,y):
    '''This is a function of addition
    :x int
    : y int
    : return x+y
    '''
    return x+y

print("name={}\ndoc={}".format(add.__name__,add.__doc__))
print(add(4,y=100))
print(add(5,1000))

================run_result==============
name=add
doc=This is a function of addition
    :x int
    : y int
    : return x+y
    
args=(4,),kwargs={'y': 100}
function:add,took:4e-06s
104
args=(5, 1000),kwargs={}
function:add,took:1e-06s
1005



解决方式二

  • 上面的解决方式,存在个副作用,函数中的属性多种多样,不可能将所有的函数都包含在新增的函数中,如果要使用的属性在新增函数中不存在,就会报错
  • 有没有比较灵活的方式呢?
  • 有,在装饰函数中添加装饰器
  • 通过copy_properties函数将被包装函数的属性覆盖掉原来的包装函数
  • 凡事被装饰的函数都要复制这些属性,这个函数很通用,python自带的函数名称为:不需要自己写
  • 可以将复制属性的函数构建成装饰器函数,带参装饰器
  • 代码实现逻辑如下:
def copy_doc_name(src):
    def doc_name(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst
    return doc_name

def logger(fn):
    @copy_doc_name(fn)
    #这里是将doc_name函数执行后的结果重新赋值给logger_1,等价式子为logger_1 = doc_name(logger_1)
    #copy_doc_name函数为嵌套函数,因此doc_name继承了fn函数,最终实现了两个函数之间属性的直接copy(赋值)
    def logger_1(*args,**kwargs):
        '''This is a logger'''
        #befpre 功能增强
        print("args={},kwargs={}".format(args,kwargs))
        start = datetime.datetime.now()
        ret = fn(*args,**kwargs)
        #after 功能增强
        duration = datetime.datetime.now()-start
        print("function:{},took:{}s".format(fn.__name__,duration.total_seconds()))
        return ret
    return logger_1

@logger
def add(x,y):
    '''This is a function of addition
    :x int
    : y int
    : return x+y
    '''
    return x+y

print("name={}\ndoc={}".format(add.__name__,add.__doc__))
print(add(4,y=100))
print(add(5,1000))

===========run_result===========
name=add
doc=This is a function of addition
    :x int
    : y int
    : return x+y
    
args=(4,),kwargs={'y': 100}
function:add,took:3e-06s
104
args=(5, 1000),kwargs={}
function:add,took:1e-06s
1005

【代码解析】

  • 执行

    @copy_doc_name(fn)

    分为两步,第一步:执行

    copy_doc_name(fn)

    这个函数,将返回值和@结合,并的呆返回值为

    doc_name

    的函数名;第二步,将@于doc_name结合,即

    @doc_name

    ,因为他放在

    logger_1

    上面,因此他省略了参数:

    logger_1

    ,也就是说:

    @doc_name

    等价于

    logger_1 = doc_name(logger_1)
  • 因为为嵌套函数,外层函数传入的fn,内层函数

    doc_name

    还可以使用,因此可以完成函数属性的替换



装饰器(有参)

  • 带参数的装饰器:
  • 他是一个函数
  • 函数作为他的形参
  • 返回值是一个不带参数的装饰器
  • 使用@functionname(参数列表)的方式调用

  • 可以看作在装饰器外层又加了一层函数
  • 装饰器无参的情况下,因为他放在add函数上面,

    @logger

    等价于

    add = logger(add)

    ,省略了add这个参数,其直接调用的是外层函数,获取外层函数的结果
  • 装饰器有参数的情况下,使用

    @copy_doc_name(fn)

    ,等价于

    @doc_name

    ,又等价于

    logger_1 = doc_name(logger_1)

    ,调用的是内层函数,返回的是内层函数执行的结果
  • 注意上面的等价式子,是

    直接将函数的return结果赋值给同名称的函数

    ,因此

    装饰器返回的是的信息都是函数对象,即函数名称

    ,要不然被更新的函数无法调用,就会报错
  • 如下面代码所示,如果装饰器中需要传多个参数,可以写成以下的代码,

    三层嵌套函数的情况下,最外层的装饰器可以传多个参数

  • 带参数的装饰器,最多三层



代码示例

需求:获取函数的执行时长,对时长超过阈值的函数记录一下

import datetime

def copy_doc_name(src):
    def doc_name(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst
    return doc_name

def logger(t1,t2):
    def logger_1(fn):
        @copy_doc_name(fn)
        def logger_2(*args,**kwargs):
            '''This is a logger'''
            #befpre 功能增强
            start = datetime.datetime.now()
            time.sleep(4)
            ret = fn(*args,**kwargs)
            #after 功能增强
            duration = (datetime.datetime.now()-start).total_seconds()
            if t2 > duration >t1::#只有执行时间大于3s,才会打印下面的语句
                print("function:{},took:{}s".format(fn.__name__,duration))
            return ret
        return logger_2
    return logger_1


@logger(3,8)  #等价于:add=logger(3)(add) ====>logger_1(add)
def add(x,y):
    '''This is a function of addition'''
    time.sleep(2)
    return x+y

print("name={}\ndoc={}".format(add.__name__,add.__doc__))
print(add(4,y=100))
print(add(5,1000))

==============run_result=============
name=add
doc=This is a function of addition
function:add,took:6.008052s
104
function:add,took:6.003909s
1005



再优化,将记录的功能提取

  • 将记录的功能提取出来,这样就可以通过外部提供的函数来灵活的控制输出
  • 代码如下:
import datetime

def copy_doc_name(src):
    def doc_name(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst
    return doc_name


def logger(t1,t2,func= lambda name,duration:print("function:{},took:{}s".format(name,duration))):
    def logger_1(fn):
        @copy_doc_name(fn)
        def logger_2(*args,**kwargs):
            '''This is a logger'''
            #befpre 功能增强
            start = datetime.datetime.now()
            time.sleep(4)
            ret = fn(*args,**kwargs)
            #after 功能增强
            duration = (datetime.datetime.now()-start).total_seconds()
            if t2 > duration >t1:
                func(fn.__name__,duration)#将打印提取出来放置在函数参数中来控制
            return ret
        return logger_2
    return logger_1


@logger(3,8)  #等价于:add=logger(3)(add) ====>logger_1(add)
def add(x,y):
    '''This is a function of addition'''
    time.sleep(2)
    return x+y

print("name={}\ndoc={}".format(add.__name__,add.__doc__))
print(add(4,y=100))
print(add(5,1000))
==============run_result===========
name=add
doc=This is a function of addition
function:add,took:6.00962s
104
function:add,took:6.009801s
1005



functors 模块的简单讲解



update_wrapper函数

  • 这是一个普通函数
  • 作用类似上面代码中的

    copy_doc_name

    函数的功能
  • 语法如下:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__','__annotations__')
WRAPPER_UPDATES = ('__dict__',)
update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES)
  • wrapper指的是包装函数(装饰器函数,可以理解为上面代码中的logger函数),wrapped指的是被包装函数(被装饰的函数,可以理解为上面代码的add函数)
  • assigned后跟一个元组,用于直接指定的属性从包装函数到被包装函数,

    WRAPPER_ASSIGNMENTS

    是要被覆盖的属性,默认指定的属性有:

    __name__



    __doc__



    __module__



    __qualname__



    __annotations__

    ;其对应的含义是函数的名称,文档,模块名,限定名,参数注解
  • updated是一个元组,

    WRAPPER_UPDATES

    中是要被更新的属性,

    __dict__

    的属性是字典,这里面是使用key-value来保存的属性,可以更新但是不能覆盖(key相同的情况下直接更新里面的值,key不相同的情况下,是在字典中新增属性)
  • 增加了一个

    __wrapped__

    属性,保留被装饰函数的原函数(即没有被logger装饰前的add函数)
  • 代码示例:
import functools
import datetime
import time

def logger(t1,t2,func= lambda name,duration:print("function:{},took:{}s".format(name,duration))):
    def logger_1(fn):
        def logger_2(*args,**kwargs):
            '''This is a logger'''
            #befpre 功能增强
            start = datetime.datetime.now()
            time.sleep(4)
            ret = fn(*args,**kwargs)
            #after 功能增强
            duration = (datetime.datetime.now()-start).total_seconds()
            if t2 > duration >t1:
                func(fn.__name__,duration)#将打印提取出来放置在函数参数中来控制
            return ret
        functools.update_wrapper(wrapper=logger_2,wrapped=fn)#functools模块的使用
        return logger_2
    return logger_1


@logger(3,8)  #等价于:add=logger(3)(add) ====>logger_1(add)
def add(x,y):
    '''This is a function of addition
    :x int
    :y int
    return x+y'''
    time.sleep(2)
    return x+y

print("name={}\ndoc={}".format(add.__name__,add.__doc__))
print("wrapped:",add.__wrapped__)#add
print(add(4,y=100))
print(add(5,1000))

=============run_result==========
name=add
doc=This is a function of addition
    :x int
    :y int
    return x+y
wrapped: <function add at 0x102a74700>
function:add,took:6.00895s
104
function:add,took:6.004358s
1005



wraps

  • 这是一个带参数的装饰器函数,
  • 使用方法如下:
import functools
import datetime
import time

def logger(t1,t2,func= lambda name,duration:print("function:{},took:{}s".format(name,duration))):
    def logger_1(fn):
        @functools.wraps(fn)#functools带参数的使用方式
        def logger_2(*args,**kwargs):
            '''This is a logger'''
            #befpre 功能增强
            start = datetime.datetime.now()
            time.sleep(4)
            ret = fn(*args,**kwargs)
            #after 功能增强
            duration = (datetime.datetime.now()-start).total_seconds()
            if t2 > duration >t1:
                func(fn.__name__,duration)#将打印提取出来放置在函数参数中来控制
            return ret
        return logger_2
    return logger_1


@logger(3,8)  #等价于:add=logger(3)(add) ====>logger_1(add)
def add(x,y):
    '''This is a function of addition
    :x int
    :y int
    return x+y'''
    time.sleep(2)
    return x+y

print("name={}\ndoc={}".format(add.__name__,add.__doc__))
print("wrapped:",add.__wrapped__)
print(add(4,y=100))
print(add(5,1000))
============run_result==========
name=add
doc=This is a function of addition
    :x int
    :y int
    return x+y
wrapped: <function add at 0x1006b4700>
function:add,took:6.00993s
104
function:add,took:6.008831s
1005



装饰器装饰类

  • 下方代码解析:
  • 下 面代码中的

    Myclass = logger(Myclass)
  • 其返回值是logger_1,因此Myclass(4,5) = logger_1(4,5)
  • 然后执行装饰器中的代码:fn(

    args,**kwargs),其中4,5被

    arg接收
  • fn是传入的参数class,因此fn(*args,**kwargs)其实就是Myclass(4,5)
  • 4,5 作为初始化函数赋值给了a和b,然后调用类中的add方法
def logger(fn):
    def logger_1(*args,**kwargs):
        print("before")
        ret = fn(*args,**kwargs)
        print("after")
        return ret
    return logger_1

@logger  # 等价于:logger(Myclass)
class Myclass:

    def __init__(self,a,b):
        self.a = a
        self.b = b

    def add(self):
        return self.a+self.b

a = Myclass(4,5).add()
print(a)

*********run_result**********
before
after
9
  • 【注意】
  • 装饰器在装饰类的时候,装饰器内层嵌套的return必须写
  • 如果是装饰函数,return并不一定要写



多个装饰器装饰一个函数

  • 从下往上装饰
  • 从上往下执行
users = {"user":"python12","pwd":"python","token":False}

def login_in(fn):
    '''
    登陆验证的装饰器
    :return:
    '''
    def ado(*args, **kwargs):
        while not users.get("token"):
            print("----------登陆页面---------")
            user_name = input("账号:")
            pwd = input("密码:")
            # 登陆验证
            if users.get("user") == user_name and\
                users.get("pwd")==pwd:
                print("登陆成功")
                users["token"] = True
                fn(*args, *kwargs)
            else:
                fn(*args, *kwargs)
    return ado

import time


def get_time(fn):
    def wapper(*args,**kwargs):
        print("AAA")
        start_t = time.time()
        fn(*args,**kwargs)
        time.sleep(3)
        end_t = time.time()
        return end_t - start_t
    return wapper

@get_time
@login_in # 多个装饰器,执行的时候从上往下调用,装饰的时候从下往上装饰
def add(x,y):
    print("num:",x+y)


a = add(4, 5)
print(a)
********************run_result***********
----------登陆页面---------
账号:python12
密码:python
登陆成功
num: 9
10.232401132583618



版权声明:本文为weixin_43754879原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。