functools
--- 可呼叫物件上的高階函式與操作¶
原始碼:Lib/functools.py
functools
模組用於高階函式:作用於或回傳其他函式的函式。一般來說,任何可呼叫物件都可以被視為用於此模組的函式。
functools
模組定義了以下函式:
- @functools.cache(user_function)¶
簡單的輕量級無繫結函式快取 (Simple lightweight unbounded function cache)。有時稱之為 "memoize"(記憶化)。
和
lru_cache(maxsize=None)
回傳相同的值,為函式引數建立一個字典查找的薄包裝器。因為它永遠不需要丟棄舊值,所以這比有大小限制的lru_cache()
更小、更快。舉例來說:
@cache def factorial(n): return n * factorial(n-1) if n else 1 >>> factorial(10) # no previously cached result, makes 11 recursive calls 3628800 >>> factorial(5) # just looks up cached value result 120 >>> factorial(12) # makes two new recursive calls, the other 10 are cached 479001600
該快取是執行緒安全的 (threadsafe),因此包裝的函式可以在多個執行緒中使用。這意味著底層資料結構在並行更新期間將保持連貫 (coherent)。
如果另一個執行緒在初始呼叫完成並快取之前進行額外的呼叫,則包裝的函式可能會被多次呼叫。
在 3.9 版被加入.
- @functools.cached_property(func)¶
將類別的一個方法轉換為屬性 (property),其值會計算一次,然後在實例的生命週期內快取為普通屬性。類似
property()
,但增加了快取機制。對於除使用該裝飾器的屬性外實質上幾乎是不可變 (immutable) 的實例,針對其所需要繁重計算會很有用。範例:
class DataSet: def __init__(self, sequence_of_numbers): self._data = tuple(sequence_of_numbers) @cached_property def stdev(self): return statistics.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_cache def count_vowels(sentence): return sum(sentence.count(vowel) for vowel in 'AEIOUaeiou')
如果 maxsize 設定為
None
,則 LRU 功能將被停用,且快取可以無限制地成長。如果 typed 設定為 true,不同型別的函式引數將會被單獨快取起來。如果 typed 為 false,則實作通常會將它們視為等效呼叫,並且僅快取單一結果。(某些型別,例如 str 和 int 可能會被單獨快取起來,即使 typed 為 false。)
請注意,型別特異性 (type specificity) 僅適用於函式的直接引數而不是其內容。純量 (scalar) 引數
Decimal(42)
和Fraction(42)
被視為具有不同結果的不同呼叫。相反地,元組引數('answer', Decimal(42))
和('answer', Fraction(42))
被視為等效。包裝的函式使用一個
cache_parameters()
函式來進行偵測,該函式回傳一個新的dict
以顯示 maxsize 和 typed 的值。這僅能顯示資訊,改變其值不會有任何效果。為了輔助測量快取的有效性並調整 maxsize 參數,包裝的函式使用了一個
cache_info()
函式來做檢測,該函式會回傳一個附名元組來顯示 hits、misses、maxsize 和 currsize。裝飾器還提供了一個
cache_clear()
函式來清除或使快取失效。原本的底層函式可以透過
__wrapped__
屬性存取。這對於要自我檢查 (introspection)、繞過快取或使用不同的快取重新包裝函式時非常有用。快取會保留對引數和回傳值的參照,直到快取過時 (age out) 或快取被清除為止。
如果方法被快取起來,則
self
實例引數將包含在快取中。請參閱如何快取方法呼叫?當最近的呼叫是即將發生之呼叫的最佳預測因子時(例如新聞伺服器上最受歡迎的文章往往每天都會發生變化),LRU (least recently used) 快取能發揮最好的效果。快取的大小限制可確保快取不會在長時間運行的行程(例如 Web 伺服器)上無限制地成長。
一般來說,僅當你想要重複使用先前計算的值時才應使用 LRU 快取。因此,快取具有 side-effects 的函式、需要在每次呼叫時建立不同可變物件的函式(例如產生器和非同步函式)或不純函式(impure function,例如 time() 或 random())是沒有意義的。
靜態網頁內容的 LRU 快取範例:
@lru_cache(maxsize=32) def get_pep(num): 'Retrieve text of a Python Enhancement Proposal' resource = f'https://peps.python.org/pep-{num:04d}' try: with urllib.request.urlopen(resource) as s: return s.read() except urllib.error.HTTPError: return 'Not Found' >>> for n in 8, 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) def fib(n): if n < 2: return n return fib(n-1) + fib(n-2) >>> [fib(n) for n in range(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_ordering class Student: def _is_valid_operand(self, other): return (hasattr(other, "lastname") and hasattr(other, "firstname")) def __eq__(self, other): if not self._is_valid_operand(other): return NotImplemented return ((self.lastname.lower(), self.firstname.lower()) == (other.lastname.lower(), other.firstname.lower())) def __lt__(self, other): if not self._is_valid_operand(other): return NotImplemented return ((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 place for positional arguments when calling
partial()
andpartialmethod()
.在 3.14 版被加入.
- functools.partial(func, /, *args, **keywords)¶
回傳一個新的 partial 物件,它在被呼叫時的行為類似於使用位置引數 args 和關鍵字引數 keywords 呼叫的 func。如果向呼叫提供更多引數,它們將被附加到 args。如果提供了額外的關鍵字引數,它們會擴充並覆寫 keywords。大致相當於:
def partial(func, /, *args, **keywords): def newfunc(*more_args, **more_keywords): return func(*args, *more_args, **(keywords | more_keywords)) newfunc.func = func newfunc.args = args newfunc.keywords = keywords return newfunc
The
partial()
function is used for partial function application which "freezes" some portion of a function's arguments and/or keywords resulting in a new object with a simplified signature. For example,partial()
can be used to create a callable that behaves like theint()
function where the base argument defaults to2
:>>> basetwo = partial(int, base=2) >>> basetwo.__doc__ = 'Convert base 2 string to an int.' >>> basetwo('10010') 18
If
Placeholder
sentinels are present in args, they will be filled first whenpartial()
is called. This makes it possible to pre-fill any positional argument with a call topartial()
; withoutPlaceholder
, only the first positional argument can be pre-filled.If any
Placeholder
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!
Calling
say_to_world('Hello')
raises aTypeError
, because only one positional argument is provided, but there are two placeholders that must be filled in.If
partial()
is applied to an existingpartial()
object,Placeholder
sentinels of the input object are filled in with new positional arguments. A placeholder can be retained by inserting a newPlaceholder
sentinel to the place held by a previousPlaceholder
:>>> from functools import partial, Placeholder as _ >>> 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
has no special treatment when used in a keyword argument topartial()
.在 3.14 版的變更: Added support for
Placeholder
in positional arguments.
- class functools.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
建構函式的 args 和 keywords 的前面。範例:
>>> class Cell: ... def __init__(self): ... self._alive = False ... @property ... def alive(self): ... return self._alive ... def set_state(self, state): ... self._alive = bool(state) ... set_alive = partialmethod(set_state, True) ... set_dead = partialmethod(set_state, False) ... >>> c = Cell() >>> c.alive False >>> c.set_alive() >>> c.alive True
在 3.4 版被加入.
- functools.reduce(function, iterable, [initial, ]/)¶
從左到右,將兩個引數的 function 累加運用到 iterable 的項目上,從而將可疊代物件減少為單一值。例如,
reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])
會計算出((((1+2)+3)+4)+5)
。左邊的引數 x 是累積值,右邊的引數 y 是來自 iterable 的更新值。如果可選的 initial 存在,則在計算中會將其放置在可疊代物件的項目之前,並在可疊代物件為空時作為預設值。如果未給定 initial 且 iterable 僅包含一個項目,則回傳第一個項目。大致相當於:
initial_missing = object() def reduce(function, iterable, initial=initial_missing, /): it = iter(iterable) if initial is initial_missing: value = next(it) else: value = initial for element in it: value = function(value, element) return value
請參閱
itertools.accumulate()
以了解產生 (yield) 所有中間值 (intermediate value) 的疊代器。
- @functools.singledispatch¶
-
若要定義泛型函式,請使用
@singledispatch
裝飾器對其裝飾。請注意,使用@singledispatch
定義函式時,分派調度 (dispatch) 是發生在第一個引數的型別上:>>> from functools import singledispatch >>> @singledispatch ... def fun(arg, verbose=False): ... if verbose: ... print("Let me just say,", end=" ") ... print(arg)
若要為函式新增過載實作,請使用泛型函式的
register()
屬性,該屬性可用作裝飾器。對於以型別來註釋的函式,裝飾器將自動推斷第一個引數的型別:>>> @fun.register ... def _(arg: int, verbose=False): ... if verbose: ... print("Strength in numbers, eh?", end=" ") ... print(arg) ... >>> @fun.register ... def _(arg: list, verbose=False): ... if verbose: ... print("Enumerate this:") ... for i, elem in enumerate(arg): ... print(i, elem)
也可以使用
types.UnionType
和typing.Union
:>>> @fun.register ... def _(arg: int | float, verbose=False): ... if verbose: ... print("Strength in numbers, eh?", end=" ") ... print(arg) ... >>> from typing import Union >>> @fun.register ... def _(arg: Union[list, set], verbose=False): ... if verbose: ... print("Enumerate this:") ... for i, elem in enumerate(arg): ... print(i, elem) ...
對於不使用型別註釋的程式碼,可以將適當的型別引數明確傳遞給裝飾器本身:
>>> @fun.register(complex) ... def _(arg, verbose=False): ... if verbose: ... print("Better than complicated.", end=" ") ... print(arg.real, arg.imag) ...
For code that dispatches on a collections type (e.g.,
list
), but wants to typehint the items of the collection (e.g.,list[int]
), the dispatch type should be passed explicitly to the decorator itself with the typehint going into the function definition:>>> @fun.register(list) ... def _(arg: list[int], verbose=False): ... if verbose: ... print("Enumerate this:") ... for i, elem in enumerate(arg): ... print(i, elem)
備註
At runtime the function will dispatch on an instance of a list regardless of the type contained within the list i.e.
[1,2,3]
will be dispatched the same as["foo", "bar", "baz"]
. The annotation provided in this example is for static type checkers only and has no runtime impact.若要啟用註冊 lambdas 和預先存在的函式,
register()
屬性也能以函式形式使用:>>> def nothing(arg, verbose=False): ... print("Nothing.") ... >>> fun.register(type(None), nothing)
register()
屬性回傳未加裝飾器的函式。這讓使得裝飾器堆疊 (decorator stacking)、pickling
以及為每個變體獨立建立單元測試成為可能:>>> @fun.register(float) ... @fun.register(Decimal) ... def fun_num(arg, verbose=False): ... if verbose: ... print("Half of your number:", end=" ") ... print(arg / 2) ... >>> fun_num is fun False
呼叫時,泛型函式會分派第一個引數的型別:
>>> 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 spam 1 spam 2 eggs 3 spam >>> fun(None) Nothing. >>> fun(1.23) 0.615
如果沒有為特定型別註冊實作,則使用其方法解析順序 (method resolution order) 來尋找更通用的實作。用
@singledispatch
裝飾的原始函式是為基底object
型別註冊的,這意味著如果沒有找到更好的實作就會使用它。如果一個實作有被註冊到一個抽象基底類別,則基底類別的虛擬子類別將被分派到該實作:
>>> from collections.abc import Mapping >>> @fun.register ... def _(arg: Mapping, verbose=False): ... if verbose: ... print("Keys & Values") ... for key, value in arg.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()
屬性現在支援以types.UnionType
和typing.Union
作為型別註釋。
- class functools.singledispatchmethod(func)¶
-
若要定義泛型方法,請使用
@singledispatchmethod
裝飾器對其裝飾。請注意,使用@singledispatchmethod
定義函式時,分派調度是發生在第一個非 self 或非 cls 引數的型別上:class Negator: @singledispatchmethod def neg(self, arg): raise NotImplementedError("Cannot negate a") @neg.register def _(self, arg: int): return -arg @neg.register def _(self, arg: bool): return not arg
@singledispatchmethod
支援與其他裝飾器巢狀使用 (nesting),例如@classmethod
。請注意,為了使dispatcher.register
可用,singledispatchmethod
必須是最外面的裝飾器。以下範例是Negator
類別,其neg
方法繫結到該類別,而不是該類別的實例:class Negator: @singledispatchmethod @classmethod def neg(cls, arg): raise NotImplementedError("Cannot negate a") @neg.register @classmethod def _(cls, arg: int): return -arg @neg.register @classmethod def _(cls, arg: bool): return not arg
相同的模式可用於其他類似的裝飾器:
@staticmethod
、@abstractmethod
等。在 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()
可以與函式以外的可呼叫物件一起使用。被包裝的物件中缺少的 assigned 或 updated 中指定的任何屬性都將被忽略(即此函式不會嘗試在包裝器函式上設定它們)。如果包裝函式本身缺少 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)
。例如:>>> from functools import wraps >>> def my_decorator(f): ... @wraps(f) ... def wrapper(*args, **kwds): ... print('Calling decorated function') ... return f(*args, **kwds) ... return wrapper ... >>> @my_decorator ... def example(): ... """Docstring""" ... print('Called example function') ... >>> example() Calling decorated function Called example function >>> example.__name__ 'example' >>> example.__doc__ 'Docstring'
如果不使用這個裝飾器工廠 (decorator factory),範例函式的名稱將會是
'wrapper'
,並且原始example()
的文件字串將會遺失。
partial
物件¶
partial
物件是由 partial()
所建立的可呼叫物件。它們有三個唯讀屬性:
partial
objects are like function objects in that they are
callable, weak referenceable, and can have attributes. There are some important
differences. For instance, the __name__
and __doc__
attributes
are not created automatically.