目录python
functools模块存放着不少工具函数,大部分都是高阶函数,其做用于或返回其余函数的函数。通常来讲,对于这个模块,任何可调用的对象均可以被视为函数。缓存
其含义是减小,它接受一个两个参数的函数,初始时从可迭代对象中取两个元素交给函数,下一次会将本次函数返回值和下一个元素传入函数进行计算,直到将可迭代对象减小为一个值,而后返回:reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates ((((1+2)+3)+4)+5)
,app
reduce(function, sequence[, initial]) -> value
下面是一个求1到100累加的栗子分布式
# 普通版 In [24]: sum = 0 In [25]: for i in range(1,101): ...: sum += i ...: In [26]: print(sum) 5050 # 利用reduce版 In [22]: import functools In [23]: functools.reduce(lambda x,y:x+y,range(101)) Out[23]: 5050
在前面学习函数参数的时候,经过设定参数的默认值,能够下降函数调用的难度。而偏函数也能够作到这一点,funtools模块中的partial方法就是将函数的部分参数固定下来
,至关于为部分的参数添加了一个固定的默认值,造成一个新的函数并返回
。从partial方法返回的函数,是对原函数的封装,是一个全新的函数。函数
注意:这里的偏函数和数学意义上的偏函数不同。工具
partial(func, *args, **keywords) - 返回一个新的被partial函数包装过的func,并带有默认值的新函数
In [27]: import functools ...: import inspect ...: ...: ...: def add(x, y): ...: return x + y ...: ...: ...: new_add = functools.partial(add,1) ...: print(new_add) ...: functools.partial(<function add at 0x000002798C757840>, 1) In [28]: In [28]: new_add(1,2) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-28-2d6520b7602a> in <module> ----> 1 new_add(1,2) TypeError: add() takes 2 positional arguments but 3 were given In [29]: new_add(1) Out[29]: 2
若是再传递两个,那么连同包装前传入的1,一块儿传给add函数,而add函数只接受两个参数,因此会报异常。学习
获取一个函数的参数列表,可使用前面学习的inspect模块测试
In [30]: inspect.signature(new_add) Out[30]: <Signature (y)>
根据前面咱们所学的函数知识,咱们知道函数传参的方式有不少种,利用偏函数包装后产生的新函数的传参会有所不一样,下面会列举不一样传参方式被偏函数包装后的签名信息。操作系统
# 最复杂的函数的形参定义方式 def add(x, y, *args, m, n, **kwargs): return x + y
functools.partial(add,x=1)
:包装后的签名信息(*, x=1, y, m, n, **kwargs),只接受keyword-only的方式赋值了functools.partial(add,1,y=20)
:包装后的签名信息(*, y=20, m, n, **kwargs),1已经被包装给x了其余参数只接受keyword-only的方式赋值了functools.partial(add,1,2,3,m=10,n=20,a=30,b=40)
:包装后的签名信息(*args, m=10, n=20, **kwargs),1给了x,2给了y, 3给了args,能够直接调用add3,而不用传递任何参数functools.partial(add,m=10,n=20,a='10')
:包装后的签名信息(x, y, *args, m=10, n=20, **kwargs),a='10'已被kwargs收集,依旧可使用位置加关键字传递实参。上面咱们已经了解了partial的基本使用,下面咱们来学习一下partial的原码,看它究竟是怎么实现的,partial的原码存在于documentation中,下面是原码:code
def partial(func, *args, **keywords): def newfunc(*fargs, **fkeywords): newkeywords = keywords.copy() # 偏函数包装时指定的位置位置参数进行拷贝 newkeywords.update(fkeywords) # 将包装完后,传递给偏函数的关键字参数更新到keyword字典中去(key相同的被替换) return func(*args, *fargs, **newkeywords) # 把偏函数包装的位置参数优先传递给被包装函数,而后是偏函数的位置参数,而后是关键字参数 newfunc.func = func # 新增函数属性,将被包装的函数绑定在了偏函数上,能够直接经过偏函数的func属性来调用原函数 newfunc.args = args # 记录包装指定的位置参数 newfunc.keywords = keywords # 记录包装指定的关键字参数 return newfunc
上面是偏函数的原码注释,若是不是很理解,请看下图
如今咱们在来看一下functools.warps函数的原码实现,前面咱们已经说明了,它是用来拷贝函数签名信息的装饰器,它在内部是使用了偏函数实现的。
def wraps(wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES): return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
使用偏函数包装了update_wrapper函数,并设置了下面参数的默认值:
'__module__', '__name__', '__qualname__', '__doc__','__annotations__'
updated=updated: 这里使用的是'__dict__'
,用来拷贝函数的属性信息
__dict__是用来存储对象属性的一个字典,其键为属性名,值为属性的值
下面来看一下update_wrapper函数,由于真正执行的就是它:
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 # 将被包装的函数,绑定在__wrapped__属性上。 return wrapper
update_wrapper返回的就是咱们的wrapper对象,因此若是不想用wraps,咱们能够直接使用update_wrapper
import time import datetime import functools def logger(fn): # @functools.wraps(fn) # wrapper = functools.wraps(fn)(wrapper) def wrapper(*args, **kwargs): start = datetime.datetime.now() res = fn(*args, **kwargs) total_seconds = (datetime.datetime.now() - start).total_seconds() print('函数:{} 执行用时:{}'.format(wrapper.__name__,total_seconds)) return res wrapper = functools.update_wrapper(wrapper, fn) # 这里进行调用,可是很难看有木有? return wrapper @logger def add(x, y): time.sleep(2) return x + y add(4,5)
这里之因此使用偏函数实现,是由于对于拷贝这个过程来讲,要拷贝的属性通常是不会改变的,那么针对这些不长改变的东西进行偏函数包装,那么在使用起来会很是方便,我以为这就是偏函数的精髓吧。
结合前面参数检查的例子,来加深functools.wraps的实现过程理解。
def check(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): sig = inspect.signature(fn) params = sig.parameters values = list(params.values()) for i, k in enumerate(args): if values[i].annotation != inspect._empty: if not isinstance(k, values[i].annotation): raise ('Key Error') for k, v in kwargs.items(): if params[k].annotation != inspect._empty: if not isinstance(v, params[k].annotation): raise ('Key Error') return fn(*args, **kwargs) return wrapper
@functools.wraps(fn)
表示一个有参装饰器,在这里实际上等于:wrapper = functools.wraps(fn)(wrapper)
functools.wraps(fn)
的返回值就是偏函数update_wrapper
, 因此也能够理解为这里实际上:update_wrapper(wrapper)
update_wrapper
在这里将wrapped的属性(也就是fn),拷贝到了wrapper上,并返回了wrapper。通过上述数说明 @functools.wraps(fn)
就等价于 wrapper = update_wrapper(wrapper)
,那么再来看拷贝的过程,就很好理解了。
学习lsu_cache方法,那么不得不提cache,那什么是cache呢?咱们说数据是存放在磁盘上的,CPU若是须要提取数据那么须要从磁盘上拿,磁盘速度很慢,直接拿的话,就很耗时间,因此操做系统会把一些数据提早存储到内存中,当CPU须要时,直接从内存中读取便可,可是内存毕竟是有限的,不是全部空间都用来存这些数据,因此内存中的一小部分用来存储磁盘上读写频繁的数据的空间,就能够简单的理解为cache(这里就不提CPU的L1,L2,L3 cache了).
lsu_cache方法简单来讲,就是当执行某一个函数时,把它的计算结果缓存到cache中,当下次调用时,就直接从缓存中拿就能够了,不用再次进行计算。这种特性对于那种计算很是耗时的场景时很是友好的。
把函数的计算结果缓存,须要的时候直接调用,这种模式该如何实现呢?简单来说就是经过一个东西来获取它对应的值,是否是和字典的元素很像?经过一个key获取它对应的value!实际上大多数缓存软件都是这种key-value结构!!!
它做为装饰器做用于须要缓存的函数,用法格式以下:
functools.lru_cache(maxsize=128, typed=False)
maxsize
:限制不一样参数和结果缓存的总量,若是设置为None
,则禁用LRU功能
,而且缓存能够无限制增加,当maxsize是二的幂时,LRU功能执行的最好,当超过maxsize设置的总数量时,LRU会把最近最少用的缓存弹出的。typed
:若是设置为True,则不一样类型的函数参数将单独缓存,例如f(3)和f(3.0)将被视为具备不一样结果的不一样调用
使用
被装饰的函数.cache_info()
来查看缓存命中的次数,以及结果缓存的数量。
In [33]: import functools In [34]: @functools.lru_cache() ...: def add(x: int, y: int) -> int: ...: time.sleep(2) ...: return x + y ...: In [35]: import time In [36]: add.cache_info() # 没有执行,没有缓存,也就没有命中了 Out[36]: CacheInfo(hits=0, misses=0, maxsize=128, currsize=0) In [37]: add(4,5) # 执行一次,缓存中不存在,因此miss1次,本次结果将会被缓存 Out[37]: 9 In [38]: add.cache_info() # 验证缓存信息,currsize表示当前缓存1个,misses表示错过1次 Out[38]: CacheInfo(hits=0, misses=1, maxsize=128, currsize=1) In [39]: add(4,5) # 本次执行速度很快,由于读取的是缓存,被命中一次,因此瞬间返回 Out[39]: 9 In [40]: add.cache_info() # 命中加1次 Out[40]: CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)
cache_info各参数含义:
def lru_cache(maxsize=128, typed=False): if maxsize is not None and not isinstance(maxsize, int): raise TypeError('Expected maxsize to be an integer or None') def decorating_function(user_function): wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo) return update_wrapper(wrapper, user_function) return decorating_function
这里的返回的 decorating_function
函数中返回的 update_wrapper
是否是看起来很熟悉,没错,这里一样利用了偏函数对被包装函数的属性签名信息进行了拷贝,而传入的wrapper是才是缓存的结果,因此咱们进一步查看_lru_cache_wrapper究竟是怎么完成缓存的。
def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo): ... ... cache = {} hits = misses = 0 full = False ... ... def wrapper(*args, **kwds): # Size limited caching that tracks accesses by recency nonlocal root, hits, misses, full key = make_key(args, kwds, typed) with lock: ... ...
这里截取部分代码进行简要说明:cache是个字典,那么就印证了以前咱们的设想,的确是使用字典key-value的形式进行缓存的。字典的key是来自于make_key函数的,那么咱们接下来看一看这个函数都作了哪些事
def _make_key(args, kwds, typed, kwd_mark = (object(),), fasttypes = {int, str, frozenset, type(None)}, tuple=tuple, type=type, len=len): key = args if kwds: # 在使用关键字传参时,遍历kwds key += kwd_mark # 使用一个特殊的对象obkect() 来 做为位置传参和关键字传参的'分隔符' 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)
_HashedSeq: 能够理解为对hash()函数的封装,仅仅是计算构建好的key的hash值,并将这个值做为key进行存储的。
注意,这里的函数_make_key是以_开头的函数,目的仅仅是告诉你,不要擅自使用,可是为了学习cache的key是怎么生成的,咱们能够直接调用它,来查看生成key的样子(这里只模拟参数的传递,理解过程便可)
In [41]: functools._make_key((1,2,3),{'a':1,'b':2},typed=False) # 不限制类型 Out[41]: [1, 2, 3, <object at 0x2798734b0b0>, 'a', 1, 'b', 2] # 缓存的key不带类型 In [49]: functools._HashedSeq(functools._make_key((1,2,3),{'a':1,'b':2},typed=True)) # 限制类型 Out[49]: [1, 2, 3, <object at 0x2798734b0b0>, 'a', 1, 'b', 2, int, int, int, int, int] # 缓存的key带类型
key构建完毕了,_HashedSeq是如何对一个列表进行hash的呢?下面来阅读如下_HashedSeq原码
class _HashedSeq(list): __slots__ = 'hashvalue' def __init__(self, tup, hash=hash): self[:] = tup self.hashvalue = hash(tup) def __hash__(self): return self.hashvalue
这里发现_HashedSeq,是一个类,当对其进行hash时,实际上调用的就是它的__hash__方法,返回的是hashvalue这个值,而这个值在__init__函数中赋值时,又来自于hash函数(这不是画蛇添足吗,哈哈),tup是元组类型,这里仍是对元组进行了hash,只是返回了一个list类型而已。这里为了测试,咱们使用_HashedSeq对象的hashvalue属性和hash函数来对比生成的hash值
In [54]: value = functools._HashedSeq(functools._make_key((1,2,3),{'a':1,'b':2},typed=True)) In [55]: value Out[55]: [1, 2, 3, <object at 0x2798734b0b0>, 'a', 1, 'b', 2, int, int, int, int, int] In [56]: value.hashvalue Out[56]: 3337684084446775700 In [57]: hash(value) Out[57]: 3337684084446775700 # 这里两次执行的结果是相同的!
小结:
最后对列表进行hash
,获得key,而后在字典中做为key对应函数的计算机结果
因为_make_key在内部是经过args和kwargs拼接来完成key的构建的,也就是说args参数位置不一样或者kwargs位置不一样,构建出来的key都不相同,那么对应的hash值也就不一样了!!!,这一点要特别注意
In [60]: add.cache_info() Out[60]: CacheInfo(hits=1, misses=1, maxsize=128, currsize=1) In [61]: add(4,5) Out[61]: 9 In [62]: add.cache_info() Out[62]: CacheInfo(hits=2, misses=1, maxsize=128, currsize=1) In [63]: add(4.0,5.0) Out[63]: 9 In [64]: add.cache_info() # 因为咱们没有对类型的限制,因此int和float构建的key是相同的,这里就命中了! Out[64]: CacheInfo(hits=3, misses=1, maxsize=128, currsize=1) In [65]: add(5,4) Out[65]: 9 In [66]: add.cache_info() # 当5,4调换时,key不一样,那么就要从新缓存了! Out[66]: CacheInfo(hits=3, misses=2, maxsize=128, currsize=2)
前面咱们讲递归的时候,使用递归的方法编写fib序列,是很是优美的可是因为每次要从新计不少值,效率很是低,若是把计算事后的值进行缓存,那么会有什么不一样的呢?
普通版: import datetime def fib(n): return 1 if n < 3 else fib(n - 1) + fib(n - 2) start = datetime.datetime.now() print(fib(40)) times = (datetime.datetime.now() - start).total_seconds() print(times) # 31.652353 lru_cache加成版本: import datetime import functools @functools.lru_cache() def fib(n): return 1 if n < 3 else fib(n - 1) + fib(n - 2) start = datetime.datetime.now() print(fib(40)) times = (datetime.datetime.now() - start).total_seconds() print(times) # 0.0
速度简直要起飞了!
lru_cache使用的前提是:
缺点:
适用场景:单机上须要空间换时间的地方,能够用缓存来将计算变成快速查询。