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,則實作通常會將它們視為等效呼叫,並且僅快取單一結果。(某些型別,例如 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):
    '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, [initial, ]/)

Apply function of two arguments cumulatively to the items of iterable, from left to right, so as to reduce the iterable to a single value. For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates ((((1+2)+3)+4)+5). The left argument, x, is the accumulated value and the right argument, y, is the update value from the iterable. If the optional initial is present, it is placed before the items of the iterable in the calculation, and serves as a default when the iterable is empty. If initial is not given and iterable contains only one item, the first item is returned.

大致相當於:

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.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 物件的行為類似於靜態方法,並且在實例屬性查找期間不會轉換為繫結方法。