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)将被视为具有不同结果的不同调用(具体可参看后面的示例)
  • 示例
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无法过期、失效
    • 不支持清除操作
    • 不支持分布式,是一个单机的缓存
  • 适用场景:单机上需要空间换时间的地方,可以用缓存来将计算变成快速的查询

参考及扩展阅读