HOME
BLOG
Python装饰器🍞
May 17 2023

锅巴py🤑

Python小白杀猪刀,装饰器

装饰器的作用就是为已经存在的对象添加额外的功能,你肯定听着很懵逼,下面从例子带你了解他的原理

go~~

plus1:

现在有如下函数,我们需要将这个函数的日志打印到终端

def add(x,y):
    print(' 我被调用执行啦 ')  # 新增打印日志语句
    return x + y

add(100,200)

# 输出
 我被调用执行啦

但是仔细思考,打印日志是一个独立的功能,它和 add 函数本身并没有什么关联关系,我们说一个函数是为完成一个工程的,所以直接写在函数里面不是不可以,但是不建议,并且打印日志属于调试信息功能,与业务无关,不应该放在业务函数加法中。
  如果不需要写在函数的里面,那么我们得想办法写在函数的外面。

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

def logger(fn, x, y):
    print(' 函数开始执行 ')
    res = fn(x, y)
    print(' 函数执行完毕 ')
    return res

logger(add,4,5)
  • 这样就解决了打印语句在函数内定义的问题了。
  • 但是如果 add 函数的形参很多,我们要挨个写上吗?所以 * args,**kwargs 帮了我们很大的忙
def add(x, y):
    return x + y

def logger(fn, *args, **kwargs):
    print(' 函数开始执行 ')
    res = fn(*args, **kwargs)
    print(' 函数执行完毕 ')
    return res

logger(add,4,5)

plus2:

有个专业术语叫做柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。(来自维基百科)

总结一下:柯里化指的就是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。其数学表达式为:

z = f(x, y) 
# 转换成 
z = f(x)(y) 
# 的形式

这里把经典的 add 函数拿来进行转换:

想要转换为 add(4)(5),那么 add(4) 必须要返回一个函数,否则无法将 5 当作参数传入

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

def new_add(x):
    def inner(y):
        return x + y
    return inner

print(add(4, 5))     # 9
print(new_add(4)(5)) # 9
print(add(4, 5) == new_add(4)(5))  # True

其实就是一个函数套函数的技巧。


plus3:

对于最开始案例的函数调用给外人看太丑了吧,因此python引入了柯里化这种思想,我们去改进一下上面的add,logger案例,让他变的更美观,更优雅

结合plus2的柯里化去改进plus1

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

def logger(fn):
    def wrapper(*args,**kwargs):
        print("函数被执行了")
        res=fn(*args,**kwargs)
        print("函数执行完毕")
        return res
    return wrapper

logger(add)(4,5)  
 #->fn接收add函数名,(4,5)是add参数,被wrapper(*args,**kwargs)接收
 #其实wrapper函数就成了add函数

​ 这样看起来是不是就好看多了?当指定 logger(add)(4,5) 时,才会打印日志,但如果想要在所有调用 add 函数的地方,我们还需要在所有调用 add 的地方修改为 logger(add)(参数), 想一想,如果我能把 logger(add) 变成 add 是不是就可以直接写成 add(4,5) 了呢?
​ 其实上面的调用形式就等于如下:

logger(add)(4,5)
--------
add = logger(add)
add(4,5)    # 将 add 重新指向了新的函数 wrapper

​ 按照柯里化的原型中 logger(add) 返回了一个函数 wrapper,而我们的(4,5) 其实是传递给了 wrapper,结合我们前面所学的高阶函数,这里的 wrapper,是一个闭包函数,因为在内部对 fn 进行了执行,这个fn接收的就是add函数名,调的就是add函数,我们在执行 wrapper 的同时,也会执行原来的函数 fn(add),并且wrapper添加了打印日志的功能

这样形式的logger 函数就是一个装饰器函数!!!装饰了add函数


当然看很多公司代码或者框架源码的时候都不会这么复杂的调用,他会利用到语法糖@去简化调用书写,一个@修饰符就是一个函数【格式:*@修饰名*】,它将被修饰的函数作为参数,并返回修饰后的同名函数或其他可调用的东西(如果返回不是一个可调用的对象那么会报错)

注意注意,python里面的变量名,变量名!看上面的例子return wrapper,就是返回了这个函数名,所以需要我们后面自己去调用,,有的人查资料看到说@语法糖会自己直接执行调用,纯属扯淡,自己调是因为他装饰器函数最后返回了一个函数调用(),而且是没参数的那种函数,,当然你直接带实参也是可以的,,不过推荐就返回函数名,下民自己调,自己把握时机

举三个代码例子就行:

无参的,返回函数调用:

def logger(fn):
    def wrapper():
        print("函数被执行了")
        res=fn()
        print("函数执行完毕")
        return res
    return wrapper()  #注意这里
 
@logger            #不用我们去调用,自动调用返回了
def add():
    print("add")

返回函数名:

def logger(fn):
    def wrapper():
        print("函数被执行了")
        res=fn()
        print("函数执行完毕")
        return res
    return wrapper  #注意这里
 
@logger
def add():
    print("add")
    
    
add()#自己调用一下

带参的返回函数调用:需要我们自己去指定实参

def logger(fn):
    def wrapper(x,y):
        print("函数被执行了")
        res=fn(x,y)
        print("函数执行完毕")
        return res
    return wrapper(2,5)  #注意这里
 
@logger
def add(x,y):
    print("add")
    print(x+y)
    return x+y

看到这应该就对@+装饰器有了基础了解了吧~~~

下面接着上面logger例子深入讲装饰器+@,改成如下样子:

def logger(fn):
    def wrapper(*args, **kwargs):
        print(' 函数被执行了 ')
        res = fn(*args, **kwargs)
        print(' 函数执行完毕 ')
        return res
    return wrapper

# 等于 add = logger(add),,@标识logger是一个装饰器,python解释器会自己去找logger函数
@logger
def add(x, y):
    return x + y

add(4,5)#直接这么调用,就可以

当解释器执行到@logger时,会自动把它下面的函数当作参数,传给 logger 函数,所以这里@logger其实就等于add = logger(add) 另外,logger 必须要定义在 add 函数之前才可以被装载!这一点很重要!


下面说一下对于使用装饰器可以做的一些改进或者问题??

利用装饰器计算如下函数的运行时间

import time
import datetime

def logger(fn):
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)
        duration = (datetime.datetime.now() - start).total_seconds()
        print(' 函数:{} 执行用时:{}'.format(wrapper.__name__, duration))
        return res                    #这里__name__ 表示当前函数的名称.
    return wrapper

@logger
def add(x, y):
    time.sleep(2)
    return x + y

# 执行结果:

In: add(4,5)
函数:wrapper 执行用时:2.000944
  • 什么鬼?这里为什么打印的是 wrapper 啊,为什么不是 add 呢?这样的话,别人不就发现我把这个函数给偷偷换掉了吗?不行不行,我得想个办法把函数的属性复制过来,由于这个功能和打印用时的装饰器不是一个功能,那么我们还得给装饰器再加一个装饰器。-_-!

    import time
    import datetime
    
    def copy_properties(old_fn):
        def wrapper(new_fn):
            new_fn.__name__ = old_fn.__name__
            return new_fn
        return wrapper
    
    def logger(fn):
        @copy_properties(fn)  # wrapper = copy_properties(fn)(wrapper)
        def wrapper(*args, **kwargs):
            start = datetime.datetime.now()
            res = fn(*args, **kwargs)
            total_seconds = (datetime.datetime.now() - start).total_seconds()
            print(' 函数:{} 执行用时:{}'.format(wrapper.__name__,total_seconds))
            return res
        return wrapper
    
    @logger
    def add(x, y):
        time.sleep(2)
        return x + y
    
    add(4,5)
    
  1. 解释器执行到@copy_properties(fn)时,会把下面的 wraper 装入,等于wrapper = copy_properties(fn)(wrapper)
  2. add就会跳到`@copy_properties,用add的名字old_fn.__name__赋值给new_fn.__name__,返回这个新名字,后面拿到的就是这个被替换的名字(狸猫换太子了属于是)
  3. 由于知道了参数的个数(一定是一个函数对象),这里就没有使用 * args, **kwargs
  4. 函数的属性不止__name__一个,其他的怎么办呢?

当然要我们在封装一个装饰器未免太麻烦了,以简便著称的python贴心的为我们提供了拷贝函数属性的内置函数

拷贝函数属性

  Python 的内置模块 functools 中,内置了很多常用的高阶函数,其中 wraps 就是用来拷贝函数的属性及签名信息的。利用 wraps,我们就不需要自己编写 copy_properties 函数了,下面是修改后的版本:

import time
import datetime
import functools

def logger(fn):
    @functools.wraps(fn)  # wrapper = functools.wraps(fn)(wrapper)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)
        total_seconds = (datetime.datetime.now() - start).total_seconds()
        print(' 函数:{} 执行用时:{}'.format(wrapper.__name__,total_seconds))
        return res
    return wrapper

@logger
def add(x, y):
    time.sleep(2)
    return x + y

add(4,5)
  • 通过使用@functools.wraps(fn) 我们可以方便的拷贝函数的属性签名信息,比如:__module____name____qualname____doc____annotations__ 等,这些属性信息,将在后续部分进行讲解,这里知道即可

上面演示的都是带一个参数的装饰器,Python称这种装饰器为无参装饰器,因为语法糖的表现形式就是 @logger, 下面要说的是带参数的装饰器,即 @logger(args)

以上述函数为例,我们需要记录当函数执行超过一定时间时的日志信息,该怎么办呢?假设这个时间是 5 秒,那么很显然,我们需要把这个时间变量传入到装饰器中进行判断。也就是说我们需要写成这种形式:

logger(5)(add)(4,5)

logger(5)返回一个函数,,其实是函数名,,之后这个函数名再(add),等于把add当参数传进去,后面跟着add的参数(4,5),

import time
import datetime
import functools

def logger(var):# logger的参数
    def inner(func): #接收add
        def wrapper(*args, **kwargs):
            start = datetime.datetime.now()
            res = func(*args, **kwargs)
            total_seconds = (datetime.datetime.now() - start).total_seconds()
            if total_seconds > var:
                print(' 函数执行时间过长 ')
            return res
        return wrapper
    return inner

@logger(5)  # logger(5)(add)(4,5)
def add(x, y):
    time.sleep(6)
    return x + y

add(4,5)
#等于不要@语法糖这么去调用,logger(5)(add)(4,5)
Python