目錄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使用的前提是:
缺點:
適用場景:單機上須要空間換時間的地方,能夠用緩存來將計算變成快速查詢。