functools模块介绍:reduce,patital,wraps
深入理解functools.lru_cache
reduce
先看示例:
import functools
print(functools.reduce(lambda x, y: x + y, [1, 2, 3, 4]))
###############输出结果##############
10
如上例子,reduce接受了两个参数,第一个参数是一个函数,第二个参数是一个列表,reduce是怎么工作的呢?
reduce会计算列表中第一个和第二个元素的和(将列表的第一个和第二个元素分别赋值给x,y),然后把lambda函数的返回值的结果和第三个元素相加(将返回值和第三个元素分别赋值给x,y),依次类推,然后再把新的这个计算结果和第四个元素相加,每一次都是上一次计算的结果和下一个元素相加,所以这样就实现了求和运算
from functools import reduce
# function 需要两参函数
# print(reduce(lambda x: x+1, [1, 2, 3, 4])) # TypeError: <lambda>() takes 1 positional argument but 2 were given
print(reduce(lambda x, y: print(f"x:{x}, y:{y}"), [1, 2, 3, 4]))
print('*' * 30)
print(reduce(lambda x, y: x + y, [1, 2, 3, 4]))
print('*' * 30)
print(reduce(lambda x, y: x + y, [1, 2, 3, 4], 100))
print(reduce(lambda x, y: print(f"x:{x}, y:{y}"), [1, 2, 3, 4], 100))
print('*' * 30)
print(reduce(lambda x, y: x - y, [1, 2, 3, 4], 100))
print('*' * 30)
# print(reduce(lambda x, y: x + y, [1, 2, 3, 4, None], 100)) # TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
print(reduce(lambda x, y: x + y, [1, 2, 3, 4], 100))
# 用sum
print(sum([1, 2, 3, 4], 100))
print(reduce(lambda x, y: x * y, [1, 2, 3, 4]))
# 无法用sum
from math import factorial
print(factorial(4))
--------------------输出结果----------------------
x:1, y:2
x:None, y:3
x:None, y:4
None
******************************
10
******************************
110
x:100, y:1
x:None, y:2
x:None, y:3
x:None, y:4
None
******************************
90
******************************
110
110
24
24
functools.reduce(function, iterable[, initializer]):
将两个参数的 function 从左至右积累地应用到 iterable 的条目,以便将该可迭代对象缩减为单一的值。 例如,reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])
是计算((((1+2)+3)+4)+5)
的值。 左边的参数 x 是积累值而右边的参数 y 则是来自 iterable 的更新值。 如果存在可选项 initializer,它会被放在参与计算的可迭代对象的条目之前,并在可迭代对象为空时作为默认值。 如果没有给出 initializer 并且 iterable 仅包含一个条目,则将返回第一项
partial
- 偏函数,把函数部分的参数固定下来,相当于为部分的参数添加了一个固定的默认值,形成一个新的函数并返回
- 从partial生成的新函数,是对原函数的封装
# 示例1
import inspect
from functools import partial
def add(x, y):
return x + y
print(partial(add, 4))
print(partial(add, 4)(5))
print('*'*30)
new_func = partial(add, 4)
print(inspect.signature(new_func))
-----------------输出结果-----------------
functools.partial(<function add at 0x10dcd9310>, 4)
9
******************************
(y)
关于函数签名可参看官方文档inspect.signature
from functools import partial
def foo(x, y):
return x - y
print(foo(4, 10))
print(foo(5, 10))
foo = partial(foo, y=10)
print(foo(4))
print(foo(5))
partial的本质
- 示例
import inspect
from functools import partial
def add(x, y, z):
return x + y + z
new_func = partial(add, y=4)
print(inspect.signature(new_func))
# print(new_func(3, 5, 6)) # TypeError: add() got multiple values for argument 'y'
print(new_func(3, z=6))
print(new_func(3, y=5, z=6))
--------------------输出结果--------------------
(x, *, y=4, z)
13
14
- 查看官方文档 可知 partial 函数大致等效于如下伪代码:
def partial(func, /, *args, **keywords): # 传入函数,位置参数和关键字参数
def newfunc(*fargs, **fkeywords): # 函数调用
newkeywords = {**keywords, **fkeywords} # 更新{**keywords}字典
return func(*args, *fargs, **newkeywords) # 原函数func函数调用
newfunc.func = func # 给newfunc动态增加func属性,保留原函数
newfunc.args = args # 保留原函数的位置参数
newfunc.keywords = keywords # 保留原函数的关键字参数
return newfunc
对比参看之前的文章函数的参数
# 再看上面的例子
def partial(func, /, *args, **keywords):
print(1, func, args, keywords) # add, (), {'y': 4}
def newfunc(*fargs, **fkeywords):
newkeywords = {**keywords, **fkeywords} # {'y':5, 'z':6}
print(2, fargs) # (3, )
print(3, newkeywords) # {'y':5, 'z':6}
return func(*args, *fargs, **newkeywords) # add(3, y=5, y=6)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc
def add(x, y, z):
return x + y + z
new_func = partial(add, y=4)
print(new_func(3, y=5, z=6))
-----------------输出结果-----------------
1 <function add at 0x1024c2e50> () {'y': 4}
2 (3,)
3 {'y': 5, 'z': 6}
14
如add函数修改成:
def add(x, y, *args, z, **kwarges):
等复杂结构,一方面我们知道partial的本质后则更容易分析,另一方面,我们可以通过签名来知道传参情况
使用偏函数partial实现functools.wraps
- functools.wraps的源码:
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
wrapper.__wrapped__ = wrapped
return wrapper
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
return partial(update_wrapper, wrapped=wrapped,
对比参看之前的文章装饰器
lru_cache
- LRU即Least-recently-used(最近最少使用),在python中lru_cache作为最近最少使用缓存装饰器来使用,内存不够就清理,内存够就暂时放到那里不清理
lru_cache(maxsize=128, typed=False)
- 如果maxsize设置为None,则禁用LRU功能。缓存可以无限制增长,当maxsize是二的幂
时,LRU功能执行得最好 - 如果typed设置为True,则不同类型的函数参数将单独缓存。例如,f(3)和f(3.0)将被视为具有不同结果的不同调用(具体可参看后面的示例)
- 如果maxsize设置为None,则禁用LRU功能。缓存可以无限制增长,当maxsize是二的幂
- 示例
import time
import functools
@functools.lru_cache()
def add(x, y):
time.sleep(2)
return x + y
start_time = time.time()
add(1, 2)
end_time = time.time()
print(f'第1次计算add(1,2)耗时{end_time-start_time}')
start_time = time.time()
add(1, 2)
end_time = time.time()
print(f'第2次计算add(1,2)耗时{end_time-start_time}')
start_time = time.time()
add(x=1, y=2)
end_time = time.time()
print(f'第1次计算add(x=1,y=2)耗时{end_time-start_time}')
start_time = time.time()
add(1, y=2)
end_time = time.time()
print(f'第1次计算add(1,y=2)耗时{end_time-start_time}')
--------------------输出结果------------------
第1次计算add(1,2)耗时2.0012688636779785
第2次计算add(1,2)耗时1.0013580322265625e-05
第1次计算add(x=1,y=2)耗时2.00421404838562
第1次计算add(1,y=2)耗时2.001896858215332
为什么可以缓存
lru_cache装饰器
- 通过一个字典缓存被装饰函数的调用和返回值
# lru_cache的源码
def lru_cache(maxsize=128, typed=False):
......
......
def decorating_function(user_function):
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
return update_wrapper(wrapper, user_function)
return decorating_function
# 接着看_lru_cache_wrapper源码
def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
......
make_key = _make_key
......
def wrapper(*args, **kwds):
......
result = user_function(*args, **kwds)
......
return result
通过分析以上代码,可以知道被lru_cache装饰器装饰的函数被包装后,可以返回其本身的调用返回值,那为什么可以缓存结果,关键代码在
_lru_cache_wrapper
中,我们可以接着分析,看看作为缓存的字典的key是怎么构造的
字典的key
# _make_key源码
def _make_key(args, kwds, typed,
kwd_mark = (object(),),
fasttypes = {int, str},
tuple=tuple, type=type, len=len):
key = args
if kwds:
key += kwd_mark # 使用kwd_mark隔开args 和 kwds参数
for item in kwds.items():
key += item
if typed:
key += tuple(type(v) for v in args)
if kwds:
key += tuple(type(v) for v in kwds.values())
elif len(key) == 1 and type(key[0]) in fasttypes:
return key[0]
return _HashedSeq(key)
_make_key(args, kwds, typed, kwd_mark = (object(),), fasttypes = {int, str},tuple=tuple, type=type, len=len)
:args,kwds,typed参数,其他均有默认值- 演示示例:来看看是怎么构造缓存字典的key的
import functools
print(functools._make_key((1, 2), {}, typed=False))
print(functools._make_key((1, ), {}, typed=False))
print(functools._make_key((), {'x': 3}, typed=False))
print(functools._make_key((1, 2), {'x': 3, 'y': 4}, typed=False))
print(functools._make_key((1, 2), {'y': 4, 'x': 3}, typed=False)) # 注意区别
print(functools._make_key((1.0, 2.0), {}, typed=True))
print(functools._make_key((1, 2), {'x': 3.0, 'y': 4.0}, typed=True))
print(functools._make_key(tuple(), {'x': 3.0, 'y': 4.0}, typed=True))
-------------------输出结果----------------------
[1, 2]
1
[<object object at 0x1073c1ea0>, 'x', 3]
[1, 2, <object object at 0x1073c1ea0>, 'x', 3, 'y', 4]
[1, 2, <object object at 0x1073c1ea0>, 'y', 4, 'x', 3]
[1.0, 2.0, <class 'float'>, <class 'float'>]
[1, 2, <object object at 0x1073c1ea0>, 'x', 3.0, 'y', 4.0, <class 'int'>, <class 'int'>, <class 'float'>, <class 'float'>]
[<object object at 0x1073c1ea0>, 'x', 3.0, 'y', 4.0, <class 'float'>, <class 'float'>]
为什么
_make_key
输出的字典key是list呢?
- list不可hash,不能作为自定的key,来分析下
_HashedSeq
源码
# _HashedSeq源码
class _HashedSeq(list):
__slots__ = 'hashvalue'
def __init__(self, tup, hash=hash):
self[:] = tup
self.hashvalue = hash(tup)
def __hash__(self):
return self.hashvalue
if __name__ == '__main__':
print(_HashedSeq((1, 2))) # 输出[1, 2]
演示示例
通过以上的源码分析,我们将很容易理解如下的例子。可在Jupyter中执行,对比查看效果
- 例1:typed的作用
import functools
import time
@functools.lru_cache()
def add(x, y):
time.sleep(2)
return x + y
print(add(1, 2))
print(add(1.0, 2.0))
print(add(1.0, 2))
@functools.lru_cache(typed=True)
def add(x, y):
time.sleep(2)
return x + y
print(add(1, 2))
print(add(1.0, 2.0))
print(add(1.0, 2))
- 例2:斐波那契数列递归方法的改造
import functools
@functools.lru_cache(maxsize=20) # 思考下maxsize对计算快慢没多大影响
def fib(n):
return 1 if n<3 else fib(n-1) + fib(n-2)
print(fib(100))
去掉lru_cache在看看效果
lru_cache装饰器应用
- 使用前提
- 同样的函数参数一定得到同样的结果
- 函数执行时间很长,且要多次执行
- 缺点
- 不支持缓存过期,key无法过期、失效
- 不支持清除操作
- 不支持分布式,是一个单机的缓存
- 适用场景:单机上需要空间换时间的地方,可以用缓存来将计算变成快速的查询