Python-高阶函数、柯里化、装饰器和functools.wraps()

1 高阶函数

1.1 高阶函数定义

First Class Object:
函数在Python中是一等公民;
函数是对象,可调用的对象;
函数可以作为普通变量、参数、返回值等;

高阶函数:
数学概念:y = g(f(x))
高阶函数应当是至少满足下面一个条件:
1.接收一个或者多个函数作为参数;
2.输出一个函数;

def counter(base):
    def inc(step=1):
        nonlocal base
        base += step
        return base
    return inc#返回的是inc函数名
#counter就是高阶函数,它满足了返回一个函数的条件。

问:f1 = counter(5)f2 = counter(5),请问f1和f2相等吗?
答:不相等,f1和f2返回的都是inc函数,而函数给不同变量赋值都是重新生成的,没有关联,所以不相等,使用==is判断都是False。
提示: == 先比较地址,不同再比较内容;is 只比较地址;

问:f1 = counter(1)然后多次执行f1(1),那么多次执行时f1调用的函数还是重新生成的吗?
答:不是,因为f1记住了赋值时生成的counter函数,而f1并没有改变索引或者被清除,所以每次调用时都是上次的counter函数。

1.2 自定义高阶函数

以前使用过内置的sorted函数,现使用高阶函数实现内置函数sorted()的功能:

def sort(iterable,*,key=None,reverse=False):
    newlist = []
    for x in iterable:
        kx = key(x) if key else x#实现key
        for i,y in enumerate(newlist):
            ky = key(y) if key else y#实现key
            flag = kx > ky if reverse else kx < ky#实现reverse
            if flag:#大的插入最前面,就地插入(降序);如果换成x < y就是(升序)排列
                newlist.insert(i,x)#在i位置插入
                break
        else:#不大于,说明是最小的,尾部追加
                newlist.append(x)
    return newlist#返回新的列表
#调用:
print(sort([1,2,5,4,3,2,'a','1'],reverse=False,key=str))

1.3 内建函数-高阶函数

sorted( )排序函数:函数对所有可迭代的对象进行排序操作。

sorted(iterable, *, key=None, reverse=False)->list#定义
#立即返回一个新的列表,对一个可迭代对象的所有元素排序,排序规则为key定义的函数,reverse表示是否排序翻转;
#list.sort(*, key=None, reverse=False)#是就地修改,不返回一个新的列表;

filter( )过滤数据函数:函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的迭代器

filter(function or None, iterable)#定义
#过滤可迭代对象的的元素,返回一个迭代器;
#function是有一个参数的函数,function(item)应返回bool值,True输出,False过滤掉;
#如果是None,则将元素等效bool值,此时只有0是False,0被过滤掉;

list(filter(lambda x:x%3==0,range(5)))#通过函数返回False的元素过滤掉;
#[0, 3]
list(filter(None,range(3)))#0被过滤掉
#[1, 2, 3, 4]

map( )映射函数:根据提供的函数对指定可迭代对象做映射。

map(function, *iterable)->map object#定义
#对多个可迭代对象的元素按照指定的函数进行映射,返回一个迭代器

dict(map(lambda x: [x,x+1],range(3)))
#{0: 1, 1: 2, 2: 3}
dict(map(lambda x: (x%5,x),range(500)))
#{0: 495, 1: 496, 2: 497, 3: 498, 4: 499}
dict(map(lambda x,y: (x,y),'abcde',range(10)))
#{'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4}

1.4 柯里化Currying

柯里化:
1. 指得是将原来接受的两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。
2. 数学公式:将z = f(x,y)转化成z = f(x)(y)的形式。

举个简单的例子,将加法函数add柯里化:

#未柯里化
def add(x,y): #z = add(x,y)
    return x + y

#柯里化
def add(x): #add(x)返回_add
    def _add(y):
        return x + y #_add函数使用x形成闭包
    return _add #add(x)->_add

#柯里化转化过程:add(x,y)->add(x)(y)
#柯里化后函数执行过程:add(x)(y)->_add(y)->return x + y

柯里化就是把1个函数转化成2个函数执行,这在后面装饰器中会用到!

2 装饰器

2.1 装饰器(无参)

装饰器(无参)
需求:一个加法函数,想增强它的功能,使其能够输出被调用过以及调用的参数信息?

def add(x, y): #未增强
   return x + y

def add(x, y): #增强
print("call add, x + y") #日志输出到控制台
return x + y

上面的加法函数是完成了需求,但是有以下的缺点:
1. 打印是一个功能,这条语句和add函数耦合太高;
2. 加法函数属于业务功能,而输出信息的功能,属于非业务功能代码,不该放在业务函数加法中;

如何分离呢?想一想使用自定义函数进行分离:

def add(x, y): #未增强
    return x + y

def logger(fn,*args,**kwargs):#fn接收函数名
    #此处省略前增强信息
    x = fn(*args,**kwargs)
    #此处省略后增强信息
    return x
print(logger(add,5,y=50))#调用

上面logger函数解决了增强和传参的问题,那能不能进一步优化呢?可以使用柯里化解决:

def add(x, y): #未增强
    return x + y

def logger(fn):
    def wrapper(*args,**kwargs):
        #此处省略前增强信息
        x = fn(*args,**kwargs)
        #此处省略后增强信息
        return x
    return wrapper
print(logger(add)(5,y=50))#调用

对于上面的调用,可不可以换一种写法呢?
先去掉print函数,对logger(add)(5,y=50)这一部分进行变形:

logger(add)(5,y=50)#将此句转换成下面两句

add = logger(add)#等价于 add = wrapper
add(5,y=50)#等价于 wrapper(5,y=50)

注意fn记住了原先的add函数,wrapper记住了fn,所以原add成了wrapper的属性保存了下来,直到wrapper的引用计数清零才消失!而只要使用新的add函数就会一直记得wrapper函数。

在Python中提供了装饰器方法,这是一个语法糖,它简化了上面变形后的2句话使用。具体使用是:

def logger(fn):#logger函数放在@logger前面
    def wrapper(*args,**kwargs):
        #此处省略前增强信息
        x = fn(*args,**kwargs)
        #此处省略后增强信息
        return x
    return wrapper

@logger #等价add = logger(add)
def add(x, y):
    return x + y

上面示例中的@logger就是装饰器语法,它简化了代码,把旧的add函数装饰后形成新的add函数。

上面的过程演示了装饰器(无参)的演化过程,这里使用的是无参语法,暂时总结下它的特点:
1. 它是一个函数;
2. 函数作为它的形参。无参装饰器实际上就是一个单形参函数;
3. 返回值也是一个函数;
3. 可以使用@functionname方式,简化调用;
注:此处装饰器的定义只是就目前所学的总结,并不准确,只是方便理解

装饰器和高阶函数:装饰器可以是高阶函数,但装饰器是对传入函数的功能的装饰(功能"增强")。

如何理解装饰器?
把原函数比作一个手机,前置功能增强相当于贴膜,后置功能增强相当于手机壳,装饰器函数就相当于装手机的盒子。

2.2 装饰器(有参)

在此之前先来了解下Python文档字符串:
1. 是在函数开头,用来解释其函数的字符串,相当于函数的帮助文档;
2. Python文档字符串叫Documentation Strings;
3. 在函数语句块的第一行,且习惯是多行的文本,所以多使用三引号;
4. 惯例是首字母大写,第一行写概述,空一行,第三行写详细描述;
5. 可以使用特殊属性__doc__访问这个文档;

举一个示例:

def add(x,y):
"""This is a function of addition"""#此句就是文档字符串doc
a = x+y
return x + y
#查看doc:
print(help(add))#help查看
print("name={}\ndoc={}".format(add.__name__, add.__doc__))#直接查看add函数名和doc

接下来我们看看上次使用的装饰器函数,这里会出现一个副作用:

def logger(fn):
    def wrapper(*args,**kwargs):
        'I am wrapper'
        #此处省略前增强信息
        x = fn(*args,**kwargs)
        #此处省略后增强信息
        return x
    return wrapper

@logger #等价add = logger(add)
def add(x, y):
    'I am add'
    return x + y
#查看add的doc和name属性:
print('name={},doc={}'.format(add.__name__,add.__doc__))
#输出:name=wrapper,doc=I am wrapper

现在能看出一个问题:在装饰add后,add本质调用的是wrapper函数,所以查看add的属性返回的是wrapper的属性。现在原函数对象的属性都被替换了,要使用装饰器且被封装前的属性也追加过去,如何解决?

先写一个自定义函数把旧函数属性赋值给新函数的属性上:

def copy_properties(src, dst): #f(x,y)的形式
    dst.__name__ = src.__name__
    dst.__doc__ = src.__doc__

#把上面函数柯里化下:
def copy_properties(src): #f(x)(y)的形式
    def _copy(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst
    return _copy

1. 通过copy_properties函数将被包装函数的属性覆盖掉包装函数;
2. 凡是被装饰的函数都需要复制这些属性,这个函数很通用;
3. 可以将复制属性的函数构建成装饰器函数,带参装饰器;

现在可以使用装饰器语法把copy_properties装饰在旧函数上:

def copy_properties(src): #f(x)(y)的形式
    def _copy(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst
    return _copy

def logger(fn):
    @copy_properties(fn)#等价于 wrapper = copy_properties(fn)(wrapper)
    def wrapper(*args,**kwargs):
        'I am wrapper'
        #此处省略前增强信息
        x = fn(*args,**kwargs)
        #此处省略后增强信息
        return x
    return wrapper

@logger #等价add = logger(add)
def add(x, y):
    'I am add'
    return x + y
#查看add的doc和name属性:
print('name={},doc={}'.format(add.__name__,add.__doc__))
#输出:name=add,doc=I am add   

上面例子中使用的装饰器(带参)类型,传入的是一个函数,返回的也是一个函数,只是在copy_properties中装饰了一下新函数wrapper,使用了旧函数fn的一些属性。
我们在logger函数下面加上copy_properties装饰器,把传入的fn函数属性装饰到wrapper上面了,这样使用新fn函数感觉和旧fn函数一样的啦!

现在把add函数功能再加强些:
  需求:获取函数的执行时长,对时长超过阈值的函数记录一下!

import datetime
import time

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

def logger(duration):
    def _logger(fn):
        @copy_properties(fn)#等价于 wrapper = copy_properties(fn)(wrapper)
        def wrapper(*args,**kwargs):
            'I am wrapper'
            start = datetime.datetime.now()#记录起始时间
            x = fn(*args,**kwargs)
            delta = (datetime.datetime.now() - start).total_seconds()#计算时间差
            print('so slow') if delta > duration else print('so fast')#阈值处理
            return x
        return wrapper
    return _logger

@logger(5) #等价add = logger(5)(add)
def add(x, y):
    'I am add'
    time.sleep(3)#人为增加函数执行时间,便于测试
    return x + y
print(add(5,6))#计算,调time函数测试阈值处理

接下来再优化下阈值处理,将记录的功能提取出来,这样就可以通过外部提供的函数来灵活的控制输出:

#提取记录功能,使用匿名函数作为func的默认值:
def logger(duration,func = lambda name,delta:print("{}took{}s".format(name,delta))):
    def _logger(fn):
        @copy_properties(fn)#等价于 wrapper = copy_properties(fn)(wrapper)
        def wrapper(*args,**kwargs):
            'I am wrapper'
            start = datetime.datetime.now()#记录起始时间
            x = fn(*args,**kwargs)
            delta = (datetime.datetime.now() - start).total_seconds()#计算时间差
            if delta > duration:#如果超出阈值,使用记录函数输出信息
                func(fn.__name__,delta)
            return x
        return wrapper
    return _logger
@logger(5)#在5后面对func传参,传一个lambda函数来改变记录功能,灵活使用记录功能

总结下带参装饰器:
1. 它是一个函数;
2. 函数作为它的形参;
3. 返回值是一个不带参的装饰器函数;
4. 使用@functionname(参数列表)方式调用;
5. 可以看做在装饰器外层又加了一层函数;

2.3 functools.wraps()

还记得前面对add函数做的属性包装吗?Python中functools模块有这样的函数满足我们:
functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) 我们来看一下.update_wrapper()的部分原码:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__')
WRAPPER_UPDATES = ('__dict__',)

1. 类似copy_properties功能;
2. wrapper:包装函数、被更新者; wrapped:被包装函数、数据源;
3. 元组WRAPPER_ASSIGNMENTS中是要被覆盖的属性,包括:__module__模块名, __name__名称, __qualname__限定名, __doc__文档, __annotations__参数注解;
4. 元组WRAPPER_UPDATES中是要被更新的属性, __dict__属性字典;
5. wrapper增加一个__wrapped__属性,保留着wrapped函数,不让wrapped函数引用计数为零;

使用functools模块再进行优化:

import functools#引用functools模块
#对核心部分的改变:
def logger(duration,func = lambda name,delta:print("{}took{}s".format(name,delta))):
    def _logger(fn):
        #@copy_properties(fn)
        def wrapper(*args,**kwargs):
            'I am wrapper'
            start = datetime.datetime.now()
            x = fn(*args,**kwargs)
            delta = (datetime.datetime.now() - start).total_seconds()
            if delta > duration:
                func(fn.__name__,delta)
            return x
        return functools.update_wrapper(wrapper,fn)#使用functools模块中的函数替代@copy_properties(fn)
    return _logger
print(add(5, 6), add.__name__, add.__doc__, sep='\n')#查看add属性

functools.update_wrapper()函数实现了我们的属性转换,甚至比自己写的更加全面,但函数使用并不简洁,Python中为此提供了装饰器用法:

import functools#引用functools模块
#对核心部分的改变:
def logger(duration,func = lambda name,delta:print("{}took{}s".format(name,delta))):
    def _logger(fn):
        @functools.wraps(fn)#等价于 wrapper = functools.wraps(fn)(wrapper)
        def wrapper(*args,**kwargs):
            'I am wrapper'
            start = datetime.datetime.now()
            x = fn(*args,**kwargs)
            delta = (datetime.datetime.now() - start).total_seconds()
            if delta > duration:
                func(fn.__name__,delta)
            return x
        return wrapper
    return _logger
print(add(5, 6), add.__name__, add.__doc__, sep='\n')#查看add属性

上面的@functools.wraps()是我们经常使用的,如果前面的不太理解,只需要记住这个即可。
看一下wraps()的部分原码:

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """
    #这里并不是柯里化,而是偏函数实现的,后面再细讲
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

请看下面的示例:

import datetime, functools
def logger(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        if delta > 3: print('too slow')
        return ret
    return wrapper
@logger
def add(x, y): pass

@logger
def sub(x, y): pass

print(add.__name__, sub.__name__)#查看add和sub的属性名

分析下面问题:
1. logger什么时候执行?
答:在碰到@logger时就立即执行。
2. logger执行过几次?
答:上面一共碰到2次@logger,一次是add上面,一次是sub上面,所以logger执行了2次。
3. wraps装饰器执行过几次?
答:在执行logger时碰到了wraps,所以会立即执行,一共碰到2次,所以wraps装饰器执行了2次。
4. wrapper的__name__等属性被覆盖过几次?
答:add和sub的wrapper是不同的,所以各自覆盖各自的属性给各自的wrapper;一共被覆盖了2次,add一次,sub一次。
5. add.name 打印什么名称?
答:add
6. sub.name 打印什么名称?
答:sub

猜你喜欢

转载自blog.csdn.net/u013340216/article/details/86549653