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)      # 沒有先前的快取結果,會進行 11 次遞迴呼叫
3628800
>>> factorial(5)       # 沒有新的呼叫,直接回傳被快取起來的結果
120
>>> factorial(12)      # 兩次新的遞迴呼叫,factorial(10) 有被快取起來
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,則實作通常會將它們視為等效呼叫,並且僅快取單一結果。(某些型別,例如 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)
def get_pep(num):
    '取得 Python 改進提案的文字'
    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(