TOC

柯里化

    在正式熟悉装饰器前,先需要了解一个思想,即柯里化,它指的是将一个接受两个形参的函数,变成一个可以接收一个参数的函数的过程,新的函数返回一个以原有第二个参数为参数的函数,这个技术我们称之为柯里化,如z=f(x,y)转换后为z=f(x)(y),根据上述公式可得知,函数的柯里化实际上就是函数闭包的一种实现,存在自由变量和函数嵌套;
    柯里化其实是函数式编程的一个过程,在这个过程中我们能把一个带有多个参数的函数转换成一系列的嵌套函数。嵌套函数的外层返回一个新函数,即内层函数,这个新函数期望传入下一个参数;
    它不断地返回新函数(像我们之前讲的,这个新函数期望当前的参数),直到所有的参数都被使用。参数会一直保持 alive (通过闭包),当柯里化函数链中最后一个函数被返回和调用的时候,它们会用于执行;
def add(x, y):
    return x + y
print(add(5, 6))  # 11
# 转换后如下
def readd(x):
    def func(y):
        return x + y  # 用到了闭包的原理,外层的x不会消亡
    return func
print(readd(5)(6)) # 11
# 或者
fn=readd(5)
print(fn(6)) # 11

偏函数

    偏函数可以将函数的一部分参数赋予默认值,那么在调用的时候,这些给定了默认值的参数我们就不需要显示的传参了,并且它还能返回一个新的函数,这个新函数,是对原函数的一种封装,之前说过,函数可以通过设定参数的默认值,从而达到降低函数调用的难度,而偏函数也可以做到这一点;
    前面说过为了形成一个单参装饰器,我们需要对其做做柯里化将其转化为单参函数,我们还可以使用偏函数,固定下来它一部分参数,就好像它有了缺省值一样,只剩下一个参数;
# 普通函数调用
def add(x, y):
    return x + y
print(add(1,2)) # 3

# 偏函数改进
from functools import partial
newadd = partial(add, y=2) # 相当于给Y赋予了一个缺省值,并得到一个新函数
print(newadd(1)) # 3 # 当y赋予了缺省值,那么我们在调用的时候只需要传入x即可
    其实我们可以理解为,partial将原函数进行了改进,将一个多参函数的参数,改为了一个具有默认值的参数的函数,其实我们也可以使用inspect进行查看改进后的函数;
from functools import partial
import inspect

def add(x, y):
    return x + y
newadd = partial(add, y=2)
print(newadd(1)) # 3

print(inspect.signature(newadd)) # (x, *, y=2)

---

def add(x, y, *args):
    return x + y + sum(args)

newadd = partial(add, 1, 2, 3, 4, 5) 
print(inspect.signature(newadd))  # (*args)

偏函数源码

    partial也是闭包的一种实现,内层函数使用来外层函数的自由变量,并且返回了一个新的函数,通过阅读一下代码即可了解偏函数的原理,固定了部分的参数,相当于给其赋予了缺省值;
def partial(func, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = keywords.copy()
        newkeywords.update(fkeywords)
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

柯里化与偏函数

    柯里化和偏函数都是用于将多个参数函数,转化为接受更少参数函数的方法,它们都能够固定参数,但是其中不同之处在于,柯里化是将函数转化为多个嵌套的一元函数,也就是每个函数只接受一个参数。
    而偏函数可以接受不只一个参数,它可以固定部分参数作为预设,还可以接受剩余的参数。

装饰器

    装饰器本质上是一个Python函数的嵌套,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如,插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用;

    函数一般是完成一组特定功能代码的封装,那么前期我们可能会遇到需求紧急的情况,项目需要快速迭代,我们就为某一需求进行单一的函数封装,但是到了后期,项目优化阶段,可能Leader提出需要对每一个逻辑代码块,进行耗时计算,统计每一个函数的执行时间,那么我们可能就需要对原有的函数进行改造,给它添加上一段执行时间的逻辑,如下代码;
    可以看到,这样虽然实现了功能,但是代码显得特别的臃肿,这仅仅是一个函数,那么如果对所有功能逻辑函数进行统计,如果按照以下的方式,可能每个函数都需要增加这么一段统计代码的逻辑,从而带来的结果就是大量的重复代码,同时也背离函数为单一功能封装的思想,并且经常性因为这种特殊需求对这些功能函数进行修修改改项目出错的可能性大大提高;
    如果又在某一天我们需要对每个函数进行日志记录,那么此时,我们可能又得修改原有的逻辑代码,就为了这一个功能,要修改整个函数的逻辑,代码的阅读性也极大降低,长此以往,重复代码将不计其数,剥离不易;
# 前期
def add(x, y):
    return x + y
# 后期
import time
def add(x, y):
    start_time = time.time()
    result = x + y
    stop_time = time.time()
    return result, stop_time - start_time
print(add(1,2)) # (3, 0.0)
    可以看到,这些代码都是一些非业务代码,我们将非业务代码塞到业务代码里面去,慢慢的代码结构就会变得极乱,所以,我们需要将这种非业务代码,进行抽离出来,因此就形成一个如下函数;
import time

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

def timer(func, *args, **kwargs):  # args用来接收位置参数,kwargs用来接收关键字参数
    start_time = time.time()
    result = func(*args, **kwargs)
    stop_time = time.time()
    return result, stop_time - start_time
print(timer(add, 1, 2))  # (3, 0.0)

柯里化改进

    此案例主要是为了递进对装饰器的认识,需要用到柯里化,可以看到上述装饰函数的实现,我们可以将上述的函数进行柯里化转换,将timer(add, 1, 2)转为timer(add)(1,2)柯里化外层函数接收一个函数对象,内层函数接收参数,实现柯里化,如下;
import time

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

def timer(func):  # func在此例中实际就是add函数
    def wrapper(*args, **kwargs): # 解构传参
        start_time = time.time()
        result = func(*args, **kwargs) # 解构传参
        stop_time = time.time()
        return result, stop_time - start_time
    return wrapper
print(timer(add)(1, 2)) # (3, 9.5367431640625e-07)
# 或
newfunc=timer(add)
print(newfunc(1,2)) # (3, 9.5367431640625e-07)
  • 说明:其实可以看到,我们的newfunc实际就是wrapper内层函数,因为wrapper可以接收任意参数,当我们传递任意参数给wrapper之后,会进行参数结构,然后将参数再传递给,我们在外层函数接收到的函数,并进行解构传参;
  • 闭包重点:可以看到,这个装饰器也存在闭包,因为内层函数引用了外层的func形参,在第一次newfunc=timer(add)进行调用的时候,其实wrapper已经消亡了,但是它所生成的对象被newfunc记住了,所以wrapper函数实际上是没有消亡的,那么我们就可以对它传参,因为wrapper里面用到了func,那也就说是内层函数func是来自于外层函数的局部变量,那么这个就形成了这个闭包;

无参装饰器

    可以看到下面装饰器调用语法add=timer(add),因为赋值语句是等号后面的先执行,所以当执行到的时候右边的add实际上是上面的add函数引用地址,所以左边的得到的结果就是timer(原add函数)的返回值,而对于左边,我们需要知道的是赋值即重新定义,所以此时,就会将add标识符的指向重新定义为'timer(原add函数)的返回值',但是老add对象(是对象,不是标识符)并没有消亡,只是add这个标识符被重新定义了,定义到了一个新的wrapper对象,所以老add函数对象是一直都存在的,被wrapper内层函数的func记住了,引用计数没有清零,只不过在全局作用域中没有标识符记住它了;
import time

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

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        stop_time = time.time()
        return result, stop_time - start_time
    return wrapper
# newfunc=timer(add)
# print(newfunc(1,2)) # (3, 9.5367431640625e-07)
add=timer(add)
print(add(1,2)) # (3, 9.5367431640625e-07)
    Python对于这些写法提供了一个装饰器语法糖,即'@装饰器函数标识符',这就是装饰器语法,拿上面的例子来举例,'add=timer(add)'等价于'@timer'(需要在需要装饰的函数上使用),这就是装饰器语法糖;
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        stop_time = time.time()
        return result, stop_time - start_time
    return wrapper

@timer # 等价于 add=timer(add),也就是@timer会将下面的函数提上来,作为timer的实参,所以下面的add就是一个新add;
def add(x, y):
    return x + y
print(add(1,2)) # (3, 2.86102294921875e-06)

带参装饰器

    上述说明了无参装饰器,无参装饰器的本质就是一个单参数函数,它就是将装饰器函数下面被装饰函数标识符提上来,作为装饰器函数的实际的参数传入,这种装饰器函数相当于一个单参的函数,它执行完成之后,又将这个返回值赋值给了下面这个被装饰函数的标识符;
    上面的装饰器,其实存在一定的问题,即文档字符串,一般用于函数的使用说明,类或者模块当中,一般在函数、类、模块等语句块的第一行,当说明文档定义好之后,可以使用属性__doc__,或者使用help内建函数访问这个文档;
def add(x, y):
    '''
    :param x: number1
    :param y: number2
    :return: number1+number2
    '''
    return x + y
print(add.__name__) # add
    通过上述,我们可以看到,使用__name__查看所调用对象的名称,即标识符,但是对于我们使用了装饰器装饰之后的函数,可能就不是这样的了,可以看到下面例子,使用add.__name__得到的是wrapper,明显存在问题,因为我们调用的是add,经过装饰器修饰之后变成了装饰器的内层函数名;
    出现这种情况的原因是,经过装饰器装饰之后,我们真正执行的被装饰的函数实际上已经不是原来的函数,而是装饰器内层函数,也就是下面例子中的wrapper,;
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        stop_time = time.time()
        return result, stop_time - start_time
    return wrapper

@timer  # 等价于 add=timer(add) => add=wrapper
def add(x, y):
    """this is add"""
    return x + y
print(add.__name__) # wrapper
    那么为了解决这种问题,我们就需要装饰器返回内层函数对象之前修改一下对象的元数据属性,下面将修改对象元数据提取为一个函数,然后在装饰器函数内部调用;
import time

def copy_properties(src,dest):  # 修改对象元数据的函数
    dest.__name__=src.__name__
    dest.__doc__=src.__doc__

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        stop_time = time.time()
        return result, stop_time - start_time
    copy_properties(func,wrapper)
    return wrapper

@timer
def add(x, y):
    """this is add"""
    return x + y
print(add.__name__) # add
    可以看到,上述将修改元信息的代码提取出来作为一个单一的函数,并且该函数为一个两参函数,这样我们就可以再一次进行改进,将其转为柯里化的形式,如下;
import time

def copy_properties(src):
    def _copy(dest):
        dest.__name__=src.__name__
        dest.__doc__=src.__doc__
    return _copy

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        stop_time = time.time()
        return result, stop_time - start_time
    copy_properties(func)(wrapper)
    return wrapper

@timer
def add(x, y):
    """this is add"""
    return x + y
print(add.__name__) # add
    因为装饰器的语法糖的意思,就是将包装函数提升上来(@copy_properties(func) 等价于  被包装的函数标识符=copy_properties(func)(被包装的函数标识符)),作为实参传入,所以我们就可以对上面的柯里化形式演进成装饰器模式,因为有两个函数,那么此时就需要使用到'带参装饰器';
import time

def copy_properties(src):
    def _copy(dest):
        dest.__name__=src.__name__
        dest.__doc__=src.__doc__
        return dest # 使用了带参装饰器,那么此处就需要返回dest
    return _copy

def timer(func):
    @copy_properties(func)   # 实际上这句就等价于wrapper=copy_properties(func)(wrapper),装饰器@语法糖,将被装饰的函数提升上来了,所以copy_properties装饰器的内层函数函数也就必须把wrapper给返回,因为它将覆盖原wrapper(wrapper=copy_properties(func)(wrapper));
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        stop_time = time.time()
        return result, stop_time - start_time
    # copy_properties(func)(wrapper)
    return wrapper

@timer
def add(x, y):
    """this is add"""
    return x + y
print(add.__name__) # add
    Python官方也考虑到了这个问题,函数一旦被装饰器装饰过,就一定会出现这个问题,因为实际执行的函数已经不是原来的那个被装饰器的函数了,所以这些这些元数据属性已经不是原来的属性了,会被覆盖,所以它提供了一个内建函数给供我们使用,functools.update_wrapper;
from functools import update_wrapper
def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        stop_time = time.time()
        return result, stop_time - start_time
    update_wrapper(wrapper=wrapper,wrapped=func)  # 在装饰器里面给定这个就可以了,详细信息可以查看源码
    return wrapper
    此外Python为这个功能也提供了一个带参装饰器,即functools.wraps,wraps内部其实就是类似柯里化的方式,但是它并非使用的是柯里化思想,而是偏函数,偏函数也会像柯里化一样返回一个新函数,所以其实本质和柯里化没太大区别。对于wraps需要知道的是它的本质实际还是调用的update_wrapper函数;
from functools import wraps
def timer(func):
    @wraps(func) # 等价于 wrapper=update_wrapper(func)(wrapper)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        stop_time = time.time()
        return result, stop_time - start_time
    return wrapper

装饰器总结

    带参装饰器,它的本质上还是一个装饰器,只不过这个参数的后面要带上一个或者多个参数,我们通过上面的案例已经基本描述明白了装饰器的内部逻辑,并且wraps其实就是Python自己提供的一个带参装饰器,查看下述代码,最终打印出了"1",通过这一点就可以清楚的知道,实际上@wapper_t1,就相当于把cce函数给重新指向了,wapper_t1(func)这个函数,虽然cce标识符已经不再指向下面的函数语句块,但是函数对象实际是每消亡的,因为cce=wapper_t1(cce),等式右边的先执行,所以在wapper_t1(cce)这条语句中的cce还是原来下面的cce函数,而左边的cce则是wapper_t1(cce)的执行结果;
    此处,语句wapper_t1(cce)的执行结果为一个值,所以我们直接打印cce就能拿到"1",如果wapper_t1的返回值是一个函数,那么就不能直接使用cce获取了,因为函数需要加括号才能执行,所以应该是cce();
def wapper_t1(func):
    return func()

@wapper_t1   # cce=wapper_t1(cce)  对cce这个标识符重新赋值,因为wapper_t1返回的不是一个函数, 所以cce就是一个最终值
def cce():
    return 1
print(cce) # 1
# ---
def wapper_t1(func):
    def inner():
        result=func()
        return result
    return inner
@wapper_t1 # cce=wapper_t1(cce)  对cce这个标识符重新赋值,因为wapper_t1返回的是一个函数, 所以cce就是一个函数
def cce():
    return 1
print(cce()) # 1
# ---
# 带参装饰器
@func # 等价于 cce=func(cce)
def cce():pass
@func() # 等价于 cce=func()(cce)
def cce():pass
@func(fn) # 等价于 cce=func(fn)(cce)
def cce():pass

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注