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

原始碼:Lib/functools.py


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

functools 模組定義了以下函式:

@functools.cache(user_function)

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

Returns the same as lru_cache(maxsize=None), creating a thin wrapper around a dictionary lookup for the function arguments. Because it never needs to evict old values, this is smaller and faster than lru_cache() with a size limit.

舉例來說:

@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() 的效果。請參閱How do I cache method calls?以了解有關這與 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,則實作通常會將它們視為等效呼叫,並且僅快取單一結果。(某些型別,例如 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 實例引數將包含在快取中。請參閱How do I cache method calls?

當最近的呼叫是即將發生之呼叫的最佳預測因子時(例如新聞伺服器上最受歡迎的文章往往每天都會發生變化),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.partial(func, /, *args, **keywords)

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

def partial(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

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

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18
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 建構函式的 argskeywords 的前面。

範例:

>>> 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[, initializer])

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

大致相當於:

def reduce(function, iterable, initializer=None):
    it = iter(iterable)
    if initializer is None:
        value = next(it)
    else:
        value = initializer
    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.UnionTypetyping.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)
...

若要啟用註冊 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.UnionTypetyping.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____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)。例如:

>>> 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.func

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

partial.args

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

partial.keywords

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

partial 物件與 function 物件類似,因為它們是可呼叫的、可弱參照的 (weak referencable) 且可以具有屬性。有一些重要的區別,例如,__name____doc__ 屬性不會自動建立。此外,類別中定義的 partial 物件的行為類似於靜態方法,並且在實例屬性查找期間不會轉換為繫結方法。