Python3標準庫之functools管理函式的工具詳解
1. functools管理函式的工具
functools模組提供了一些工具來調整或擴充套件函式和其他callable物件,從而不必完全重寫。
1.1 修飾符
functools模組提供的主要工具就是partial類,可以用來“包裝”一個有預設引數的callable物件。得到的物件本身就是callable,可以把它看作是原來的函式。它與原函式的引數完全相同,呼叫時還可以提供額外的位置或命名函式。可以使用partial而不是lambda為函式提供預設引數,有些引數可以不指定。
1.1.1 部分物件
第一個例子顯示了函式myfunc()的兩個簡單partial物件。show_details()的輸出中包含這個部分物件(partial object)的func、args和keywords屬性。
import functools def myfunc(a,b=2): "Docstring for myfunc()." print(' called myfunc with:',(a,b)) def show_details(name,f,is_partial=False): "Show details of a callable object." print('{}:'.format(name)) print(' object:',f) if not is_partial: print(' __name__:',f.__name__) if is_partial: print(' func:',f.func) print(' args:',f.args) print(' keywords:',f.keywords) return show_details('myfunc',myfunc) myfunc('a',3) print() # Set a different default value for 'b',but require # the caller to provide 'a'. p1 = functools.partial(myfunc,b=4) show_details('partial with named default',p1,True) p1('passing a') p1('override b',b=5) print() # Set default values for both 'a' and 'b'. p2 = functools.partial(myfunc,'default a',b=99) show_details('partial with defaults',p2,True) p2() p2(b='override b') print() print('Insufficient arguments:') p1()
在這個例子的最後,呼叫了之前建立的第一個partial,但沒有為a傳入一個值,這便會導致一個異常。
1.1.2 獲取函式屬性
預設的,partial物件沒有__name__或__doc__屬性。如果沒有這些屬性,被修飾的函式將更難除錯。使用update_wrapper()可以從原函式將屬性複製或增加到partial物件。
import functools def myfunc(a,f): "Show details of a callable object." print('{}:'.format(name)) print(' object:',f) print(' __name__:',end=' ') try: print(f.__name__) except AttributeError: print('(no __name__)') print(' __doc__',repr(f.__doc__)) print() show_details('myfunc',myfunc) p1 = functools.partial(myfunc,b=4) show_details('raw wrapper',p1) print('Updating wrapper:') print(' assign:',functools.WRAPPER_ASSIGNMENTS) print(' update:',functools.WRAPPER_UPDATES) print() functools.update_wrapper(p1,myfunc) show_details('updated wrapper',p1)
增加到包裝器的屬性在WRAPPER_ASSIGNMENTS中定義,另外WARPPER_UPDATES列出了要修改的值。
1.1.3 其他callable
partial適用於任何callable物件,而不只是獨立的函式。
import functools class MyClass: "Demonstration class for functools" def __call__(self,e,f=6): "Docstring for MyClass.__call__" print(' called object with:',(self,f)) def show_details(name,repr(f.__doc__)) return o = MyClass() show_details('instance',o) o('e goes here') print() p = functools.partial(o,e='default for e',f=8) functools.update_wrapper(p,o) show_details('instance wrapper',p) p()
這個例子從一個包含__call__()方法的類例項中建立部分物件。
1.1.4 方法和函式
partial()返回一個可以直接使用的callable,partialmethod()返回的callable則可以用作物件的非繫結方法。在下面的例子中,這個獨立函式兩次被增加為MyClass的屬性,一次使用partialmethod()作為method1(),另一次使用partial()作為method2()。
import functools def standalone(self,a=1,b=2): "Standalone function" print(' called standalone with:',a,b)) if self is not None: print(' self.attr =',self.attr) class MyClass: "Demonstration class for functools" def __init__(self): self.attr = 'instance attribute' method1 = functools.partialmethod(standalone) method2 = functools.partial(standalone) o = MyClass() print('standalone') standalone(None) print() print('method1 as partialmethod') o.method1() print() print('method2 as partial') try: o.method2() except TypeError as err: print('ERROR: {}'.format(err))
method1()可以從MyClass的一個例項中呼叫,這個例項作為第一個引數傳入,這與採用通常方法定義的方法是一樣的。method2()未被定義為繫結方法,所以必須顯式傳遞self引數;否則,這個呼叫會導致TypeError。
1.1.5 獲取修飾符的函式屬性
更新所包裝callable的屬性對修飾符尤其有用,因為轉換後的函式最後會得到原“裸”函式的屬性。
import functools def show_details(name,repr(f.__doc__)) print() def simple_decorator(f): @functools.wraps(f) def decorated(a='decorated defaults',b=1): print(' decorated:',b)) print(' ',end=' ') return f(a,b=b) return decorated def myfunc(a,b=2): "myfunc() is not complicated" print(' myfunc:',b)) return # The raw function show_details('myfunc',myfunc) myfunc('unwrapped,default b') myfunc('unwrapped,passing b',3) print() # Wrap explicitly wrapped_myfunc = simple_decorator(myfunc) show_details('wrapped_myfunc',wrapped_myfunc) wrapped_myfunc() wrapped_myfunc('args to wrapped',4) print() # Wrap with decorator syntax @simple_decorator def decorated_myfunc(a,b): myfunc(a,b) return show_details('decorated_myfunc',decorated_myfunc) decorated_myfunc() decorated_myfunc('args to decorated',4)
functools提供了一個修飾符wraps(),它會對所修飾的函式應用update_wrapper()。
1.2 比較
在Python 2中,類可以定義一個__cmp__()方法,它會根據這個物件小於、對於或者大於所比較的元素而分別返回-1、0或1.Python 2.1引入了富比較(rich comparison)方法API(__lt__()、__le__()、__eq__()、__ne__()、__gt__()和__ge__()) ,可以完成一個比較操作並返回一個布林值。Python 3廢棄了__cmp__()而代之以這些新的方法,另外functools提供了一些工具,從而能更容易地編寫符合新要求的類,即符合Python 3中新的比較需求。
1.2.1 富比較
設計富比較API是為了支援涉及複雜比較的類,以最高效的方式實現各個測試。不過,如果比較相對簡單的類,就沒有必要手動地建立各個富比價方法了。total_ordering()類修飾符可以為一個提供了部分方法的類增加其餘的方法。
import functools import inspect from pprint import pprint @functools.total_ordering class MyObject: def __init__(self,val): self.val = val def __eq__(self,other): print(' testing __eq__({},{})'.format( self.val,other.val)) return self.val == other.val def __gt__(self,other): print(' testing __gt__({},other.val)) return self.val > other.val print('Methods:\n') pprint(inspect.getmembers(MyObject,inspect.isfunction)) a = MyObject(1) b = MyObject(2) print('\nComparisons:') for expr in ['a < b','a <= b','a == b','a >= b','a > b']: print('\n{:<6}:'.format(expr)) result = eval(expr) print(' result of {}: {}'.format(expr,result))
這個類必須提供__eq__()和另外一個富比較方法的實現。這個修飾符會增加其餘方法的實現,它們會使用所提供的比較。如果無法完成一個比較,這個方法應當返回NotImplemented,從而在另一個物件上使用逆比較操作符嘗試比較,如果仍無法比較,便會完全失敗。
1.2.2 比對序
由於Python 3廢棄了老式的比較函式,sort()之類的函式中也不再支援cmp引數。對於使用了比較函式的較老的程式,可以用cmp_to_key()將比較函式轉換為一個返回比較鍵(collation key)的函式,這個鍵用於確定元素在最終序列中的位置。
import functools class MyObject: def __init__(self,val): self.val = val def __str__(self): return 'MyObject({})'.format(self.val) def compare_obj(a,b): """Old-style comparison function. """ print('comparing {} and {}'.format(a,b)) if a.val < b.val: return -1 elif a.val > b.val: return 1 return 0 # Make a key function using cmp_to_key() get_key = functools.cmp_to_key(compare_obj) def get_key_wrapper(o): "Wrapper function for get_key to allow for print statements." new_key = get_key(o) print('key_wrapper({}) -> {!r}'.format(o,new_key)) return new_key objs = [MyObject(x) for x in range(5,-1)] for o in sorted(objs,key=get_key_wrapper): print(o)
正常情況下,可以直接使用cmp_to_key(),不過這個例子中引入了一個額外的包裝器函式,這樣呼叫鍵函式時可以列印更多的資訊。
如輸出所示,sorted()首先對序列中的每一個元素呼叫get_key_wrapper()以生成一個鍵。cmp_to_key()返回的鍵是functools中定義的一個類的例項,這個類使用傳入的老式比較函式實現富比較API。所有鍵都建立之後,通過比較這些鍵來對序列排序。
1.3 快取
lru_cache()修飾符將一個函式包裝在一個“最近最少使用的”快取中。函式的引數用來建立一個雜湊鍵,然後對映到結果。後續的呼叫如果有相同的引數,就會從這個快取獲取值而不會再次呼叫函式。這個修飾符還會為函式增加方法來檢查快取的狀態(cache_info())和清空快取(cache_clear())。
import functools @functools.lru_cache() def expensive(a,b): print('expensive({},{})'.format(a,b)) return a * b MAX = 2 print('First set of calls:') for i in range(MAX): for j in range(MAX): expensive(i,j) print(expensive.cache_info()) print('\nSecond set of calls:') for i in range(MAX + 1): for j in range(MAX + 1): expensive(i,j) print(expensive.cache_info()) print('\nClearing cache:') expensive.cache_clear() print(expensive.cache_info()) print('\nThird set of calls:') for i in range(MAX): for j in range(MAX): expensive(i,j) print(expensive.cache_info())
這個例子在一組巢狀迴圈中執行了多個expensive()呼叫。第二次呼叫時有相同的引數值,結果在快取中。清空快取並再次執行迴圈時,這些值必須重新計算。
為了避免一個長時間執行的程序導致快取無限制的擴張,要指定一個最大大小。預設為128個元素,不過對於每個快取可以用maxsize引數改變這個大小。
import functools @functools.lru_cache(maxsize=2) def expensive(a,b): print('called expensive({},b)) return a * b def make_call(a,b): print('({},b),end=' ') pre_hits = expensive.cache_info().hits expensive(a,b) post_hits = expensive.cache_info().hits if post_hits > pre_hits: print('cache hit') print('Establish the cache') make_call(1,2) make_call(2,3) print('\nUse cached items') make_call(1,3) print('\nCompute a new value,triggering cache expiration') make_call(3,4) print('\nCache still contains one old item') make_call(2,3) print('\nOldest item needs to be recomputed') make_call(1,2)
在這個例子中,快取大小設定為2個元素。使用第3組不同的引數(3,4)時,快取中最老的元素會被清除,代之以這個新結果。
lru_cache()管理的快取中鍵必須是可雜湊的,所以對於用快取查詢包裝的函式,它的所有引數都必須是可雜湊的。
import functools @functools.lru_cache(maxsize=2) def expensive(a,b) post_hits = expensive.cache_info().hits if post_hits > pre_hits: print('cache hit') make_call(1,2) try: make_call([1],2) except TypeError as err: print('ERROR: {}'.format(err)) try: make_call(1,{'2': 'two'}) except TypeError as err: print('ERROR: {}'.format(err))
如果將一個不能雜湊的物件傳入這個函式,則會產生一個TypeError。
1.4 縮減資料集
reduce()函式取一個callable和一個數據序列作為輸入。它會用這個序列中的值呼叫這個callable,並累加得到的輸出來生成單個值作為輸出。
import functools def do_reduce(a,b): print('do_reduce({},b)) return a + b data = range(1,5) print(data) result = functools.reduce(do_reduce,data) print('result: {}'.format(result))
這個例子會累加序列中的數。
可選的initializer引數放在序列最前面,像其他元素一樣處理。可以利用這個引數以新輸入更新前面計算的值。
import functools def do_reduce(a,data,99) print('result: {}'.format(result))
在這個例子中,使用前面的總和99來初始化reduce()計算的值。
如果沒有initializer引數,那麼只有一個元素的序列會自動縮減為這個值。空列表會生成一個錯誤,除非提供一個initializer引數。
import functools def do_reduce(a,b)) return a + b print('Single item in sequence:',functools.reduce(do_reduce,[1])) print('Single item in sequence with initializer:',[1],99)) print('Empty sequence with initializer:',[],99)) try: print('Empty sequence:',[])) except TypeError as err: print('ERROR: {}'.format(err))
由於initializer引數相當於一個預設值,但也要與新值結合(如果輸入序列不為空),所以必須仔細考慮這個引數的使用是否適當,這很重要。如果預設值與新值結合沒有意義,那麼最好是捕獲TypeError而不是傳入一個initializer引數。
1.5 泛型函式
在類似Python的動態型別語言中,通常需要基於引數的型別完成稍有不同的操作,特別是在處理元素列表與單個元素的差別時。直接檢查引數的型別固然很簡單,但是有些情況下,行為差異可能被隔離到單個的函式中,對於這些情況,functools提供了singledispatch()修飾符來註冊一組泛型函式(generic function),可以根據函式第一個引數型別自動切換。
import functools @functools.singledispatch def myfunc(arg): print('default myfunc({!r})'.format(arg)) @myfunc.register(int) def myfunc_int(arg): print('myfunc_int({})'.format(arg)) @myfunc.register(list) def myfunc_list(arg): print('myfunc_list()') for item in arg: print(' {}'.format(item)) myfunc('string argument') myfunc(1) myfunc(2.3) myfunc(['a','b','c'])
新函式的register()屬性相當於另一個修飾符,用於註冊替代實現。用singledispatch()包裝的第一個函式是預設實現,在未指定其他型別特定函式時就使用這個預設實現,在這個例子中特定型別就是float。
沒有找到這個型別的完全匹配時,會計算繼承順序,並使用最接近的匹配型別。
import functools class A: pass class B(A): pass class C(A): pass class D(B): pass class E(C,D): pass @functools.singledispatch def myfunc(arg): print('default myfunc({})'.format(arg.__class__.__name__)) @myfunc.register(A) def myfunc_A(arg): print('myfunc_A({})'.format(arg.__class__.__name__)) @myfunc.register(B) def myfunc_B(arg): print('myfunc_B({})'.format(arg.__class__.__name__)) @myfunc.register(C) def myfunc_C(arg): print('myfunc_C({})'.format(arg.__class__.__name__)) myfunc(A()) myfunc(B()) myfunc(C()) myfunc(D()) myfunc(E())
在這個例子中,類D和E與已註冊的任何泛型函式都不完全匹配,所選擇的函式取決於類層次結構。
總結
到此這篇關於Python3標準庫之functools管理函式的工具詳解的文章就介紹到這了,更多相關Python3 functools管理函式工具內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!