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装饰器应用
-
使用前提
- 同样的函数参数一定得到同样的结果,至少是一段时间内,同样输入得到同样结果
- 计算代价高,函数执行时间很长
- 需要多次执行,每一次计算代价高
-
本质是建立函数调用的参数到返回值的映射
-
缺点
- 不支持缓存过期,key无法过期、失效
- 不支持清除操作
- 不支持分布式,是一个单机的缓存
-
lru_cache适用场景,单机上需要空间换时间的地方,可以用缓存来将计算变成快速的查询
学习lru_cache可以让我们了解缓存背后的原理。
-------------------------------------------
个性签名:独学而无友,则孤陋而寡闻。做一个灵魂有趣的人!
如果觉得这篇文章对你有小小的帮助的话,记得在右下角点个“推荐”哦,博主在此感谢!
万水千山总是情,打赏一分行不行,所以如果你心情还比较高兴,也是可以扫码打赏博主,哈哈哈(っ•̀ω•́)っ✎⁾⁾!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix