functools --- 可呼叫物件上的高階函式與操作

原始碼:Lib/functools.py


functools 模組用於高階函式:作用於或回傳其他函式的函式。一般來說,任何可呼叫物件都可以被視為用於此模組的函式。

functools 模組定義了以下函式:

@functools.cache(user_function)

簡單的輕量級無繫結函式快取 (Simple lightweight unbounded function cache)。有時稱之為"memoize"(記憶化)

lru_cache(maxsize=None) 回傳相同的值,為函式引數建立一個字典查找的薄包裝器。因為它永遠不需要丟棄舊值,所以這比有大小限制的lru_cache() 更小、更快。

舉例來說:

@cachedeffactorial(n):returnn*factorial(n-1)ifnelse1>>>factorial(10)# 沒有先前的快取結果,會進行 11 次遞迴呼叫3628800>>>factorial(5)# 只查詢快取的結果120>>>factorial(12)# 進行兩次新的遞迴呼叫,其他 10 次是快取的479001600

該快取是執行緒安全的 (threadsafe),因此包裝的函式可以在多個執行緒中使用。這意味著底層資料結構在並行更新期間將保持連貫 (coherent)。

如果另一個執行緒在初始呼叫完成並快取之前進行額外的呼叫,則包裝的函式可能會被多次呼叫。

在 3.9 版被加入.

@functools.cached_property(func)

將類別的一個方法轉換為屬性 (property),其值會計算一次,然後在實例的生命週期內快取為普通屬性。類似property(),但增加了快取機制。對於除使用該裝飾器的屬性外實質上幾乎是不可變 (immutable) 的實例,針對其所需要繁重計算會很有用。

範例:

classDataSet:def__init__(self,sequence_of_numbers):self._data=tuple(sequence_of_numbers)@cached_propertydefstdev(self):returnstatistics.stdev(self._data)

cached_property() 的機制與property() 有所不同。除非定義了 setter,否則常規屬性會阻止屬性的寫入。相反地,cached_property 則允許寫入。

cached_property 裝飾器僅在查找時且僅在同名屬性不存在時運行。當它運行時,cached_property 會寫入同名的屬性。後續屬性讀取和寫入優先於cached_property 方法,並且它的工作方式與普通屬性類似。

可以透過刪除屬性來清除快取的值,這使得cached_property 方法可以再次運行。

cached_property 無法防止多執行緒使用中可能出現的競爭條件 (race condition)。getter 函式可以在同一個實例上運行多次,最後一次運行會設定快取的值。所以快取的屬性最好是冪等的 (idempotent),或者在一個實例上運行多次不會有害,就不會有問題。如果同步是必要的,請在裝飾的 getter 函式內部或在快取的屬性存取周圍實作必要的鎖。

請注意,此裝飾器會干擾PEP 412 金鑰共用字典的操作。這意味著實例字典可能比平常佔用更多的空間。

此外,此裝飾器要求每個實例上的__dict__ 屬性是可變對映 (mutable mapping)。這意味著它不適用於某些型別,例如元類別 (metaclass)(因為型別實例上的__dict__ 屬性是類別命名空間的唯讀代理),以及那些指定__slots__ 而不包含__dict__ 的型別作為有定義的插槽之一(因為此種類別根本不提供__dict__ 屬性)。

如果可變對映不可用或需要金鑰共享以節省空間,則也可以透過在lru_cache() 之上堆疊property() 來實作類似於cached_property() 的效果。請參閱如何快取方法呼叫?以了解有關這與cached_property() 間不同之處的更多詳細資訊。

在 3.8 版被加入.

在 3.12 版的變更:在 Python 3.12 之前,cached_property 包含一個未以文件記錄的鎖,以確保在多執行緒使用中能保證 getter 函式對於每個實例只會執行一次。然而,鎖是針對每個屬性,而不是針對每個實例,這可能會導致無法被接受的嚴重鎖爭用 (lock contention)。在 Python 3.12+ 中,此鎖已被刪除。

functools.cmp_to_key(func)

將舊式比較函式轉換為鍵函式,能與接受鍵函式的工具一起使用(例如sorted()min()max()heapq.nlargest()heapq.nsmallest()itertools.groupby())。此函式主要作為轉換工具,用於從有支援使用比較函式的 Python 2 轉換成的程式。

比較函式是任何能接受兩個引數、對它們進行比較,並回傳負數(小於)、零(相等)或正數(大於)的可呼叫物件。鍵函式是接受一個引數並回傳另一個用作排序鍵之值的可呼叫物件。

範例:

sorted(iterable,key=cmp_to_key(locale.strcoll))# locale-aware sort order

有關排序範例和簡短的排序教學,請參閱排序技法

在 3.2 版被加入.

@functools.lru_cache(user_function)
@functools.lru_cache(maxsize=128,typed=False)

以記憶化可呼叫物件來包裝函式的裝飾器,最多可省去maxsize 個最近的呼叫。當使用相同引數定期呼叫繁重的或 I/O 密集的函式時,它可以節省時間。

該快取是執行緒安全的 (threadsafe),因此包裝的函式可以在多個執行緒中使用。這意味著底層資料結構在並行更新期間將保持連貫 (coherent)。

如果另一個執行緒在初始呼叫完成並快取之前進行額外的呼叫,則包裝的函式可能會被多次呼叫。

由於字典用於快取結果,因此函式的位置引數和關鍵字引數必須是可雜湊的

不同的引數模式可以被認為是具有不同快取條目的不同呼叫。例如,f(a=1,b=2)f(b=2,a=1) 的關鍵字引數順序不同,並且可能有兩個不同的快取條目。

如果指定了user_function,則它必須是個可呼叫物件。這使得lru_cache 裝飾器能夠直接應用於使用者函式,將maxsize 保留為其預設值 128:

@lru_cachedefcount_vowels(sentence):returnsum(sentence.count(vowel)forvowelin'AEIOUaeiou')

如果maxsize 設定為None,則 LRU 功能將被停用,且快取可以無限制地成長。

如果typed 設定為 true,不同型別的函式引數將會被單獨快取起來。如果typed 為 false,則實作通常會將它們視為等效呼叫,並且僅快取單一結果。(某些型別,例如strint 可能會被單獨快取起來,即使typed 為 false。)

請注意,型別特異性 (type specificity) 僅適用於函式的直接引數而不是其內容。純量 (scalar) 引數Decimal(42)Fraction(42) 被視為具有不同結果的不同呼叫。相反地,元組引數('answer',Decimal(42))('answer',Fraction(42)) 被視為等效。

包裝的函式使用一個cache_parameters() 函式來進行偵測,該函式回傳一個新的dict 以顯示maxsizetyped 的值。這僅能顯示資訊,改變其值不會有任何效果。

為了輔助測量快取的有效性並調整maxsize 參數,包裝的函式使用了一個cache_info() 函式來做檢測,該函式會回傳一個附名元組來顯示hitsmissesmaxsizecurrsize

裝飾器還提供了一個cache_clear() 函式來清除或使快取失效。

原本的底層函式可以透過__wrapped__ 屬性存取。這對於要自我檢查 (introspection)、繞過快取或使用不同的快取重新包裝函式時非常有用。

快取會保留對引數和回傳值的參照,直到快取過時 (age out) 或快取被清除為止。

如果方法被快取起來,則self 實例引數將包含在快取中。請參閱如何快取方法呼叫?

當最近的呼叫是即將發生之呼叫的最佳預測因子時(例如新聞伺服器上最受歡迎的文章往往每天都會發生變化),LRU (least recently used) 快取能發揮最好的效果。快取的大小限制可確保快取不會在長時間運行的行程(例如 Web 伺服器)上無限制地成長。

一般來說,僅當你想要重複使用先前計算的值時才應使用 LRU 快取。因此,快取具有 side-effects 的函式、需要在每次呼叫時建立不同可變物件的函式(例如產生器和非同步函式)或不純函式(impure function,例如 time() 或 random())是沒有意義的。

靜態網頁內容的 LRU 快取範例:

@lru_cache(maxsize=32)defget_pep(num):'取得 Python 改進提案的文字'resource=f'https://peps.python.org/pep-{num:04d}'try:withurllib.request.urlopen(resource)ass:returns.read()excepturllib.error.HTTPError:return'Not Found'>>>fornin8,290,308,320,8,218,320,279,289,320,9991:...pep=get_pep(n)...print(n,len(pep))>>>get_pep.cache_info()CacheInfo(hits=3,misses=8,maxsize=32,currsize=8)

使用快取來實作動態規劃 (dynamic programming) 技法以有效率地計算費波那契數 (Fibonacci numbers) 的範例:

@lru_cache(maxsize=None)deffib(n):ifn<2:returnnreturnfib(n-1)+fib(n-2)>>>[fib(n)forninrange(16)][0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610]>>>fib.cache_info()CacheInfo(hits=28,misses=16,maxsize=None,currsize=16)

在 3.2 版被加入.

在 3.3 版的變更:新增typed 選項。

在 3.8 版的變更:新增user_function 選項。

在 3.9 版的變更:新增cache_parameters() 函式。

@functools.total_ordering

給定一個定義一個或多個 rich comparison 排序方法的類別,該類別裝飾器會提供其餘部分。這簡化了指定所有可能的 rich comparison 操作所涉及的工作:

類別必須定義__lt__()__le__()__gt__()__ge__() 其中之一。此外,該類別應該提供__eq__() 方法。

舉例來說:

@total_orderingclassStudent:def_is_valid_operand(self,other):return(hasattr(other,"lastname")andhasattr(other,"firstname"))def__eq__(self,other):ifnotself._is_valid_operand(other):returnNotImplementedreturn((self.lastname.lower(),self.firstname.lower())==(other.lastname.lower(),other.firstname.lower()))def__lt__(self,other):ifnotself._is_valid_operand(other):returnNotImplementedreturn((self.lastname.lower(),self.firstname.lower())<(other.lastname.lower(),other.firstname.lower()))

備註

雖然此裝飾器可以輕鬆建立能好好運作的完全有序型別 (totally ordered types),但它的確以衍生比較方法的執行速度較慢和堆疊追蹤 (stack trace) 較複雜做為其代價。如果效能基準測試顯示這是給定應用程式的效能瓶頸,那麼實作全部六種 rich comparison 方法通常能輕鬆地提升速度。

備註

此裝飾器不會嘗試覆寫類別或其超類別 (superclass)中宣告的方法。這意味著如果超類別定義了比較運算子,total_ordering 將不會再次實作它,即使原始方法是抽象的。

在 3.2 版被加入.

在 3.4 版的變更:現在支援從底層對於未識別型別的比較函式回傳NotImplemented

functools.Placeholder

A singleton object used as a sentinel to reserve a placefor positional arguments when callingpartial()andpartialmethod().

在 3.14 版被加入.

functools.partial(func,/,*args,**keywords)

回傳一個新的partial 物件,它在被呼叫時的行為類似於使用位置引數args 和關鍵字引數keywords 呼叫的func。如果向呼叫提供更多引數,它們將被附加到args。如果提供了額外的關鍵字引數,它們會擴充並覆寫keywords。大致相當於:

defpartial(func,/,*args,**keywords):defnewfunc(*more_args,**more_keywords):returnfunc(*args,*more_args,**(keywords|more_keywords))newfunc.func=funcnewfunc.args=argsnewfunc.keywords=keywordsreturnnewfunc

partial() 用於部分函式應用程序,它「凍結」函式引數和/或關鍵字的某些部分,從而產生具有簡化簽名的新物件。例如,partial() 可用來建立可呼叫函式,其行為類似於int() 函式,其中base 引數預設為 2:

>>>basetwo=partial(int,base=2)>>>basetwo.__doc__='Convert base 2 string to an int.'>>>basetwo('10010')18

IfPlaceholder sentinels are present inargs, they will be filled firstwhenpartial() is called. This makes it possible to pre-fill any positionalargument with a call topartial(); withoutPlaceholder,only the chosen number of leading positional arguments can be pre-filled.

If anyPlaceholder sentinels are present, all must be filled at call time:

>>>say_to_world=partial(print,Placeholder,Placeholder,"world!")>>>say_to_world('Hello','dear')Hello dear world!

Callingsay_to_world('Hello') raises aTypeError, becauseonly one positional argument is provided, but there are two placeholdersthat must be filled in.

Ifpartial() is applied to an existingpartial() object,Placeholder sentinels of the input object are filled in withnew positional arguments.A placeholder can be retained by inserting a newPlaceholder sentinel to the place held by a previousPlaceholder:

>>>fromfunctoolsimportpartial,Placeholderas_>>>remove=partial(str.replace,_,_,'')>>>message='Hello, dear dear world!'>>>remove(message,' dear')'Hello, world!'>>>remove_dear=partial(remove,_,' dear')>>>remove_dear(message)'Hello, world!'>>>remove_first_dear=partial(remove_dear,_,1)>>>remove_first_dear(message)'Hello, dear world!'

Placeholder cannot be passed topartial() as a keyword argument.

在 3.14 版的變更:Added support forPlaceholder in positional arguments.

classfunctools.partialmethod(func,/,*args,**keywords)

回傳一個新的partialmethod 描述器 (descriptor),其行為類似於partial,只不過它被設計為用於方法定義而不能直接呼叫。

func 必須是一個descriptor 或可呼叫物件(兩者兼具的物件,就像普通函式一樣,會被當作描述器處理)。

func 是描述器時(例如普通的 Python 函式、classmethod()staticmethod()abstractmethod()partialmethod 的另一個實例),對__get__ 的呼叫將被委託 (delegated) 給底層描述器,且一個適當的partial 物件會被作為結果回傳。

func 是非描述器可呼叫物件 (non-descriptor callable) 時,會動態建立適當的繫結方法 (bound method)。當被作為方法使用時,其行為類似於普通的 Python 函式:self 引數將作為第一個位置引數插入,甚至會在提供給partialmethod 建構函式的argskeywords 的前面。

範例:

>>>classCell:...def__init__(self):...self._alive=False...@property...defalive(self):...returnself._alive...defset_state(self,state):...self._alive=bool(state)...set_alive=partialmethod(set_state,True)...set_dead=partialmethod(set_state,False)...>>>c=Cell()>>>c.aliveFalse>>>c.set_alive()>>>c.aliveTrue

在 3.4 版被加入.

functools.reduce(function,iterable,/[,initial])

從左到右,將兩個引數的function 累加運用到iterable 的項目上,從而將可疊代物件減少為單一值。例如,reduce(lambdax,y:x+y,[1,2,3,4,5]) 會計算出((((1+2)+3)+4)+5)。左邊的引數x 是累積值,右邊的引數y 是來自iterable 的更新值。如果可選的initial 存在,則在計算中會將其放置在可疊代物件的項目之前,並在可疊代物件為空時作為預設值。如果未給定initialiterable 僅包含一個項目,則回傳第一個項目。

大致相當於:

initial_missing=object()defreduce(function,iterable,/,initial=initial_missing):it=iter(iterable)ifinitialisinitial_missing:value=next(it)else:value=initialforelementinit:value=function(value,element)returnvalue

請參閱itertools.accumulate() 以了解產生 (yield) 所有中間值 (intermediate value) 的疊代器。

在 3.14 版的變更:initial is now supported as a keyword argument.

@functools.singledispatch

將函式轉換為單一調度泛型函式

若要定義泛型函式,請使用@singledispatch 裝飾器對其裝飾。請注意,使用@singledispatch 定義函式時,分派調度 (dispatch) 是發生在第一個引數的型別上:

>>>fromfunctoolsimportsingledispatch>>>@singledispatch...deffun(arg,verbose=False):...ifverbose:...print("Let me just say,",end=" ")...print(arg)

若要為函式新增過載實作,請使用泛型函式的register() 屬性,該屬性可用作裝飾器。對於以型別來註釋的函式,裝飾器將自動推斷第一個引數的型別:

>>>@fun.register...def_(arg:int,verbose=False):...ifverbose:...print("Strength in numbers, eh?",end=" ")...print(arg)...>>>@fun.register...def_(arg:list,verbose=False):...ifverbose:...print("Enumerate this:")...fori,eleminenumerate(arg):...print(i,elem)

也可以使用typing.Union

>>>@fun.register...def_(arg:int|float,verbose=False):...ifverbose:...print("Strength in numbers, eh?",end=" ")...print(arg)...>>>fromtypingimportUnion>>>@fun.register...def_(arg:Union[list,set],verbose=False):...ifverbose:...print("Enumerate this:")...fori,eleminenumerate(arg):...print(i,elem)...

對於不使用型別註釋的程式碼,可以將適當的型別引數明確傳遞給裝飾器本身:

>>>@fun.register(complex)...def_(arg,verbose=False):...ifverbose:...print("Better than complicated.",end=" ")...print(arg.real,arg.imag)...

For code that dispatches on a collections type (e.g.,list), but wantsto typehint the items of the collection (e.g.,list[int]), thedispatch type should be passed explicitly to the decorator itself with thetypehint going into the function definition:

>>>@fun.register(list)...def_(arg:list[int],verbose=False):...ifverbose:...print("Enumerate this:")...fori,eleminenumerate(arg):...print(i,elem)

備註

At runtime the function will dispatch on an instance of a list regardlessof the type contained within the list i.e.[1,2,3] will bedispatched the same as["foo","bar","baz"]. The annotationprovided in this example is for static type checkers only and has noruntime impact.

若要啟用註冊lambdas 和預先存在的函式,register() 屬性也能以函式形式使用:

>>>defnothing(arg,verbose=False):...print("Nothing.")...>>>fun.register(type(None),nothing)

register() 屬性回傳未加裝飾器的函式。這讓使得裝飾器堆疊 (decorator stacking)、pickling 以及為每個變體獨立建立單元測試成為可能:

>>>@fun.register(float)...@fun.register(Decimal)...deffun_num(arg,verbose=False):...ifverbose:...print("Half of your number:",end=" ")...print(arg/2)...>>>fun_numisfunFalse

呼叫時,泛型函式會分派第一個引數的型別:

>>>fun("Hello, world.")Hello, world.>>>fun("test.",verbose=True)Let me just say, test.>>>fun(42,verbose=True)Strength in numbers, eh? 42>>>fun(['spam','spam','eggs','spam'],verbose=True)Enumerate this:0 spam1 spam2 eggs3 spam>>>fun(None)Nothing.>>>fun(1.23)0.615

如果沒有為特定型別註冊實作,則使用其方法解析順序 (method resolution order) 來尋找更通用的實作。用@singledispatch 裝飾的原始函式是為基底object 型別註冊的,這意味著如果沒有找到更好的實作就會使用它。

如果一個實作有被註冊到一個抽象基底類別,則基底類別的虛擬子類別將被分派到該實作:

>>>fromcollections.abcimportMapping>>>@fun.register...def_(arg:Mapping,verbose=False):...ifverbose:...print("Keys & Values")...forkey,valueinarg.items():...print(key,"=>",value)...>>>fun({"a":"b"})a => b

若要檢查泛型函式將為給定型別選擇哪種實作,請使用dispatch() 屬性:

>>>fun.dispatch(float)<function fun_num at 0x1035a2840>>>>fun.dispatch(dict)# note: default implementation<function fun at 0x103fe0000>

若要存取所有已註冊的實作,請使用唯讀registry 屬性:

>>>fun.registry.keys()dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,          <class 'decimal.Decimal'>, <class 'list'>,          <class 'float'>])>>>fun.registry[float]<function fun_num at 0x1035a2840>>>>fun.registry[object]<function fun at 0x103fe0000>

在 3.4 版被加入.

在 3.7 版的變更:register() 屬性現在支援使用型別註釋。

在 3.11 版的變更:register() 屬性現在支援使用typing.Union 型別註釋。

classfunctools.singledispatchmethod(func)

將方法轉換為單一調度泛型函式

若要定義泛型方法,請使用@singledispatchmethod 裝飾器對其裝飾。請注意,使用@singledispatchmethod 定義函式時,分派調度是發生在第一個非self 或非cls 引數的型別上:

classNegator:@singledispatchmethoddefneg(self,arg):raiseNotImplementedError("Cannot negate a")@neg.registerdef_(self,arg:int):return-arg@neg.registerdef_(self,arg:bool):returnnotarg

@singledispatchmethod supports nesting with other decorators such as@classmethod. Note that to allow fordispatcher.register,singledispatchmethod must be theouter mostdecorator. Here is theNegator class with theneg methods bound tothe class, rather than an instance of the class:

classNegator:@singledispatchmethod@classmethoddefneg(cls,arg):raiseNotImplementedError("Cannot negate a")@neg.register@classmethoddef_(cls,arg:int):return-arg@neg.register@classmethoddef_(cls,arg:bool):returnnotarg

The same pattern can be used for other similar decorators:@staticmethod,@~abc.abstractmethod, and others.

在 3.8 版被加入.

functools.update_wrapper(wrapper,wrapped,assigned=WRAPPER_ASSIGNMENTS,updated=WRAPPER_UPDATES)

更新wrapper 函式,使其看起來像wrapped 函式。可選引數是元組,用於指定原始函式的哪些屬性直接賦值給包裝函式上的匹配屬性,以及包裝函式的哪些屬性使用原始函式中的對應屬性進行更新。這些引數的預設值是模組層級的常數WRAPPER_ASSIGNMENTS(它賦值給包裝函式的__module____name____qualname____annotations____type_params____doc__ 文件字串 (docstring))和WRAPPER_UPDATES(更新包裝器函式的__dict__,即實例字典)。

為了允許出於內省 (introspection) 和其他目的所對原始函式的存取(例如繞過快取裝飾器,如lru_cache()),此函式會自動向包裝器新增__wrapped__ 屬性,該包裝器參照被包裝的函式。

此函式的主要用途是在decorator 函式中,它包裝函式並回傳包裝器。如果包裝器函式未更新,則回傳函式的元資料 (metadata) 將反映包裝器定義而非原始函式定義,這通常不太會有幫助。

update_wrapper() 可以與函式以外的可呼叫物件一起使用。被包裝的物件中缺少的assignedupdated 中指定的任何屬性都將被忽略(即此函式不會嘗試在包裝器函式上設定它們)。如果包裝函式本身缺少updated 中指定的任何屬性,仍然會引發AttributeError

在 3.2 版的變更:現在會自動新增__wrapped__ 屬性。現在預設會複製__annotations__ 屬性。缺少的屬性不再觸發AttributeError

在 3.4 版的變更:__wrapped__ 屬性現在都會參照包裝函式,即便函式有定義__wrapped__ 屬性。(參見bpo-17482

在 3.12 版的變更:現在預設會複製__type_params__ 屬性。

@functools.wraps(wrapped,assigned=WRAPPER_ASSIGNMENTS,updated=WRAPPER_UPDATES)

這是一個方便的函式,用於在定義包裝器函式時呼叫update_wrapper() 作為函式裝飾器。它相當於partial(update_wrapper,wrapped=wrapped,assigned=assigned,updated=updated)。例如:

>>>fromfunctoolsimportwraps>>>defmy_decorator(f):...@wraps(f)...defwrapper(*args,**kwds):...print('Calling decorated function')...returnf(*args,**kwds)...returnwrapper...>>>@my_decorator...defexample():..."""Docstring"""...print('Called example function')...>>>example()Calling decorated functionCalled example function>>>example.__name__'example'>>>example.__doc__'Docstring'

如果不使用這個裝飾器工廠 (decorator factory),範例函式的名稱將會是'wrapper',並且原始example() 的文件字串將會遺失。

partial 物件

partial 物件是由partial() 所建立的可呼叫物件。它們有三個唯讀屬性:

partial.func

一個可呼叫的物件或函式。對partial 物件的呼叫將被轉送到帶有新引數和關鍵字的func

partial.args

最左邊的位置引數將會被加入到提供給partial 物件呼叫的位置引數的前面。

partial.keywords

呼叫partial 物件時將提供的關鍵字引數。

partial 物件與函式物件類似,因為它們是可呼叫的、可弱參照的 (weak referencable) 且可以具有屬性的。但有一些重要的區別,例如,__name____doc__ 屬性不會自動建立。