09.python高阶函数、柯里化、装饰器、无参装饰器、有参装饰器、functools之reduce partial lru_cache

高阶函数

一等公民

  • 函数在Python是一等公民(First-Class Object)
  • 函数也是对象,是可调用对象
  • 函数可以作为普通变量,也可以作为函数的参数、返回值

高阶函数

高阶函数(High-order Function)

  • 数学概念 y = f(g(x))
  • 在数学和计算机科学中,高阶函数应当是至少满足下面一个条件的函数
    • 接受一个或多个函数作为参数
    • 输出一个函数

观察下面的函数定义,回答问题

def counter(base):
    def inc(step=1):
        nonlocal base
        base += step
        return base
    return inc

if __name__ == '__main__':
    f=counter(12)
    print(f())
  • 请问counter是不是高阶函数?是
  • 上面代码有没有问题?如果有,如何改?有
  • 如何调用以完成计数功能?
  • f1 = counter(5)和f2=counter(5),请问f1和f2相等吗?
def counter(base):
    def inc(step=1): # 有没有闭包?
		nonlocal base # 形参base也是外部函数counter的local变量
		base += step
		return base
  	return inc
c1 = counter(5)
print(c1())
print(c1())
print(c1())
f1 = counter(5)
f2 = counter(5)
print(f1 == f2) # 相等吗?

柯里化

  • 指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有
  • 第二个参数为参数的函数
  • z = f(x, y) 转换成 z = f(x)(y)的形式

例如

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

原来函数调用为 add(4, 5) ,柯里化目标是 add(4)(5) 。如何实现?
每一次括号说明是函数调用,说明 add(4)(5) 是2次函数调用。

add(4)(5)
等价于
t = add(4)
t(5)

也就是说add(4)应该返回函数。

def add(x):
    def _add(y):
        return x + y
    return _add
add(100, 200)

通过嵌套函数就可以把函数转成柯里化函数

def add(x, y, z):
    return x + y + z
# 练习,对add柯里化后,可以分别得到下面三种调用方式
add(4)(5, 6)
add(4, 5)(6)
add(4)(5)(6)

装饰器

需求:为一个加法函数增加记录实参的功能

def add(x, y):
    print('add called. x={}, y={}'.format(x, y)) # 增加的记录功能
    return x + y
add(4, 5)

上面的代码满足了需求,但有缺点:
记录信息的功能,可以是一个单独的功能。显然和add函数耦合太紧密。加法函数属于业务功能,输出
信息属于非功能代码,不该放在add函数中

1、提供一个函数logger完成记录功能

def add(x, y):
    return x + y
def logger(fn):
    print('调用前增强')
    ret = fn(4, 5)
    print('调用后增强')
    return ret
print(logger(add))

2、改进传参

def add(x, y):
    return x + y
def logger(fn, *args, **kwargs):
    print('调用前增强')
    ret = fn(*args, **kwargs) # 参数解构
    print('调用后增强')
    return ret
print(logger(add, 4, 5))

3、柯里化

def add(x, y):
    return x + y
def logger(fn):
    def wrapper(*args, **kwargs):
        print('调用前增强')
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        return ret
    return wrapper

调用

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

或者

inner = logger(add) x = inner(4, 5)
print(x)

再进一步

def add(x, y):
    return x + y
def logger(fn):
    def wrapper(*args, **kwargs):
        print('调用前增强')
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        return ret
    return wrapper
add = logger(add)
print(add(100, 200))

4、装饰器语法

def logger(fn):
    def wrapper(*args, **kwargs):
        print('调用前增强')
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        return ret
    return wrapper
@logger # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    return x + y
print(add(100, 200))

@logger就是装饰器语法
等价式非常重要,如果你不能理解装饰器,开始的时候一定要把等价式写在后面

无参装饰器

  • 上例的装饰器语法,称为无参装饰器
  • @符号后是一个函数
  • 虽然是无参装饰器,但是@后的函数本质上是单参函数
  • 上例的logger函数是一个高阶函数

日志记录装饰器实现

import time
import datetime
def logger(fn):
    def wrapper(*args, **kwargs):
        print('调用前增强')
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {} took {}s.'.format(fn.__name__, delta))
        return ret
    return wrapper
@logger # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    time.sleep(2)
    return x + y
print(add(100, 200))

文档字符串

  • Python文档字符串Documentation Strings
  • 在函数(类、模块)语句块的第一行,且习惯是多行的文本,所以多使用三引号
  • 文档字符串也算是合法的一条语句
  • 惯例是首字母大写,第一行写概述,空一行,第三行写详细描述
  • 可以使用特殊属性__doc__访问这个文档
def add(x, y):
    """这是加法函数的文档"""
    return x + y
print("{}'s doc = {}".format(add.__name__ , add.__doc__))
import time
import datetime
def logger(fn):
    def wrapper(*args, **kwargs):
        "wrapper's doc"
        print('调用前增强')
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {} took {}s.'.format(fn.__name__, delta))
        return ret
    return wrapper
@logger # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    """add's doc"""
    time.sleep(0.1)
    return x + y
print("name={}, doc={}".format(add.__name__ , add.__doc__))

被装饰后,函数名和文档都不对了。如何解决?

functools模块提供了一个wraps装饰器函数,本质上调用的是update_wrapper,它就是一个属性复制函数。

wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

  • wrapped就是被包装函数
  • wrapper就是包装函数
  • 用被包装函数的属性覆盖包装函数的同名属性
  • 元组WRAPPER_ASSIGNMENTS中是要被覆盖的属性
    • module , name , qualname , doc , annotations
    • 模块名、名称、限定名、文档、参数注解
import time
import datetime
from functools import wraps
def logger(fn):
    @wraps(fn) # 用被包装函数fn的属性覆盖包装函数wrapper的同名属性
    def wrapper(*args, **kwargs): # wrapper = wraps(fn)(wrapper)
        "wrapper's doc"
        print('调用前增强')
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs) # 参数解构
        print('调用后增强')
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {} took {}s.'.format(fn.__name__, delta))
        return ret
    return wrapper
@logger # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    """add's doc"""
    time.sleep(0.1)
    return x + y
print("name={}, doc={}".format(add.__name__ , add.__doc__))

带参装饰器

  • @之后不是一个单独的标识符,是一个函数调用
  • 函数调用的返回值又是一个函数,此函数是一个无参装饰器
  • 带参装饰器,可以有任意个参数
    • @func()
    • @func(1)
    • @func(1, 2)

进阶

import datetime
from functools import wraps
def logger(fn):
    @wraps(fn) # 用被包装函数fn的属性覆盖包装函数wrapper的同名属性
    def wrapper(*args, **kwargs): # wrapper = wraps(fn)(wrapper)
        "wrapper's doc"
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs) # 参数解构
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {} took {}s.'.format(fn.__name__, delta))
        return ret
    return wrapper
@logger # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    """add function"""
@logger
def sub(x, y):
    """sub function"""
print(add.__name__, sub.__name__)
  • logger什么时候执行?
  • logger执行过几次?
  • wraps装饰器执行过几次?
  • wrapper的 name 等属性被覆盖过几次?
  • add.name 打印什么名称?
  • sub.name 打印什么名称?

functools模块

reduce

  • functools.reduce(function, iterable[, initial])
  • 就是减少的意思
  • 初始值没有提供就在可迭代对象中取一个
from functools import reduce
s = sum(range(10))
print(s) s = reduce(lambda x: x, range(10))
print(s) # TypeError: <lambda>() takes 1 positional argument but 2 were given

从上面的异常推断lambda应该2个参数

s = reduce(lambda x,y: print(x,y), range(10))
print(s)
返回结果如下
0 1
None 2
None 3
None 4
None 5
None 6
None 7
None 8
None 9
None

上一次lambda函数返回值会成为下一次的x

from functools import reduce

s = sum(range(10))
print(s) 

s = reduce(lambda x,y: x + y, range(10), 100)
print(s)

sum只能求和,reduce能做更加复杂的迭代计算。

思考:5的阶乘怎么做?

from functools import reduce
reduce(lambda x,y:x *y ,range(1,6),1)

partial

偏函数

  • 把函数部分参数固定下来,相当于为部分的参数添加了固定的默认值,形成一个新的函数,并返回这个新函数
  • 这个新函数是对原函数的封装
from functools import partial
def add(x, y):
    return x + y
newadd = partial(add, y=5)
print(newadd(4))
print(newadd(4, y=15))
print(newadd(x=4))
print(newadd(4, 6)) # 可以吗
print(newadd(y=6, x=4))
import inspect
print(inspect.signature(newadd))
from functools import partial
def add(x, y, *args):
    return x + y + sum(args)
newadd = partial(add, 1, 2, 3, 4, 5)
print(newadd())
print(newadd(1))
print(newadd(1, 2))
print(newadd(x=1)) #
print(newadd(x=1, y=2)) #
import inspect
print(inspect.signature(newadd))

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
def add(x, y):
    return x + y
foo = partial(add, 4)
foo(5)

尝试分析functools.wraps的实现。类别下面的实现

from functools import partial, wraps
import inspect
def add(a, b, c, d):
    return a + b + c + d
newadd = partial(add, b=2, c=3, d=4)
print(inspect.signature(newadd))

Lru_cache

@functools.lru_cache(maxsize=128, typed=False)

  • lru即Least-recently-used,最近最少使用。cache缓存
  • 如果maxsize设置为None,则禁用LRU功能,并且缓存可以无限制增长。当maxsize是二的幂时,LRU功能执行得最好
  • 如果typed设置为True,则不同类型的函数参数将单独缓存。例如,f(3)和f(3.0)将被视为具有不同结果的不同调用
  • Python 3.8 简化了调用,可以使用
@functools.lru_cache
def add():
    pass
# 等价于
@functools.lru_cache(128)
def add():
    pass
from functools import lru_cache
import time
@lru_cache()
def add(x, y=5):
    print('-' * 30)
    time.sleep(3)
    return x + y
print(1, add(4, 5))
print(2, add(4, 5))
print(3, add(x=4, y=5))
print(4, add(y=5, x=4))
print(5, add(4.0, 5))
print(6, add(4))

到底什么调用才能用缓存呢?

lru_cache本质

  • 内部使用了一个字典
  • key是由_make_key函数构造出来
from functools import _make_key
print(_make_key((4, 5), {}, False))
print(_make_key((4, 5), {}, True))
print(_make_key((4,), {'y':5}, False))
print(_make_key((), {'x':4, 'y':5}, False))
print(_make_key((), {'y':5, 'x':4}, False))

应用

# 斐波那契数列lru_cache版
from functools import lru_cache
@lru_cache()
def fib(n):
    return 1 if n < 3 else fib(n-1) + fib(n-2)
print(fib(101))

总结

lru_cache装饰器应用

  • 使用前提

    1. 同样的函数参数一定得到同样的结果,至少是一段时间内,同样输入得到同样结果
    2. 计算代价高,函数执行时间很长
    3. 需要多次执行,每一次计算代价高
  • 本质是建立函数调用的参数到返回值的映射

  • 缺点

    • 不支持缓存过期,key无法过期、失效
    • 不支持清除操作
    • 不支持分布式,是一个单机的缓存
  • lru_cache适用场景,单机上需要空间换时间的地方,可以用缓存来将计算变成快速的查询

学习lru_cache可以让我们了解缓存背后的原理。

posted on 2022-01-13 10:01  无语至极  阅读(84)  评论(0编辑  收藏  举报

导航