functools — 可呼叫物件上的高階函式和操作

原始碼: Lib/functools.py


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

functools 模組定義了以下函式

@functools.cache(user_function)

簡單的輕量級無界函式快取。有時稱為 “記憶化”

返回值與 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

快取是執行緒安全的,因此包裝後的函式可以在多個執行緒中使用。這意味著底層資料結構在併發更新期間將保持一致。

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

在 3.9 版本中新增。

@functools.cached_property(func)

將類的方法轉換為屬性,其值計算一次,然後在例項的生命週期內作為普通屬性進行快取。類似於 property(),增加了快取功能。對於例項的昂貴計算屬性(否則實際上是不可變的)很有用。

示例

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 不能防止多執行緒使用中可能發生的競爭條件。getter 函式可能會在同一例項上執行多次,最近一次執行會設定快取的值。如果快取的屬性是冪等的,或者多次在例項上執行不會造成危害,那麼這沒問題。如果需要同步,請在修飾的 getter 函式內部或快取的屬性訪問周圍實現必要的鎖定。

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

此外,此裝飾器要求每個例項上的 __dict__ 屬性是可變的對映。這意味著它將無法與某些型別一起使用,例如元類(因為型別例項上的 __dict__ 屬性是類名稱空間的只讀代理),以及那些指定 __slots__ 但不包括 __dict__ 作為已定義槽之一的型別(因為此類根本不提供 __dict__ 屬性)。

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

在 3.8 版本中新增。

在 3.12 版本中更改: 在 Python 3.12 之前,cached_property 包含一個未記錄的鎖,以確保在多執行緒使用中,getter 函式保證每個例項只執行一次。但是,鎖是按屬性而不是按例項的,這可能會導致不可接受的高鎖爭用。在 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 限制的函式使用相同引數被週期性呼叫時,它可以節省時間。

快取是執行緒安全的,因此包裝後的函式可以在多個執行緒中使用。這意味著底層資料結構在併發更新期間將保持一致。

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

由於使用字典來快取結果,所以傳遞給函式的位置引數和關鍵字引數必須是可雜湊的

不同的引數模式可能被視為具有單獨快取條目的不同調用。例如,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,則實現通常會將它們視為等效呼叫,並且僅快取單個結果。(即使 typed 為 false,某些型別(例如 strint)也可能被單獨快取。)

請注意,型別特異性僅適用於函式的直接引數,而不是它們的內容。標量引數 Decimal(42)Fraction(42) 將被視為具有不同結果的不同調用。相反,元組引數 ('answer', Decimal(42))('answer', Fraction(42)) 將被視為等效。

包裝後的函式配備了一個 cache_parameters() 函式,該函式返回一個新的 dict,其中顯示了 maxsizetyped 的值。這僅用於資訊目的。更改這些值無效。

為了幫助衡量快取的效率並調整 maxsize 引數,包裝後的函式配備了一個 cache_info() 函式,該函式返回一個 命名元組,顯示 hitsmissesmaxsizecurrsize

裝飾器還提供了一個 cache_clear() 函式,用於清除或使快取無效。

原始的基礎函式可以透過 __wrapped__ 屬性訪問。這對於內省、繞過快取或使用不同的快取重新包裝函式很有用。

快取保留對引數和返回值的引用,直到它們從快取中過期或直到快取被清除。

如果快取了一個方法,則 self 例項引數將包含在快取中。請參閱如何快取方法呼叫?

LRU (最近最少使用) 快取在最近的呼叫是即將到來的呼叫的最佳預測器時效果最佳(例如,新聞伺服器上最受歡迎的文章每天都會發生變化)。快取的大小限制確保快取在長時間執行的程序(如 Web 伺服器)上不會無限增長。

通常,LRU 快取僅應在您想重用先前計算的值時使用。因此,快取具有副作用的函式、需要在每次呼叫時建立不同的可變物件的函式(例如生成器和非同步函式)或諸如 time() 或 random() 之類的非純函式沒有意義。

靜態 Web 內容的 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)

使用快取實現動態程式設計技術來有效計算斐波那契數的示例

@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

給定一個定義了一個或多個富比較排序方法的類,此類裝飾器會提供其餘的方法。這簡化了指定所有可能的富比較操作的工作。

該類必須定義 __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()))

注意

雖然此裝飾器可以輕鬆建立行為良好的完全排序型別,但它確實以派生的比較方法的執行速度較慢和堆疊跟蹤更復雜為代價。如果效能基準測試表明這是給定應用程式的瓶頸,那麼實現所有六個富比較方法可能會提供簡單的速度提升。

注意

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

在 3.2 版本中新增。

在 3.4 版本中更改: 現在支援從底層比較函式為無法識別的型別返回 NotImplemented

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

返回一個新的partial 物件,該物件在呼叫時將像呼叫帶有位置引數 args 和關鍵字引數 keywordsfunc 一樣。如果為呼叫提供了更多引數,則它們會附加到 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 描述符,其行為類似於 partial,但它被設計為用作方法定義,而不是直接可呼叫的。

func 必須是描述符或可呼叫物件(兩者都是的物件,如普通函式,被視為描述符)。

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

func 是一個非描述符可呼叫物件時,會動態建立一個合適的繫結方法。當用作方法時,它的行為類似於普通的 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, ]/)

從左到右累積地將 function 應用於 iterable 的項,以便將可迭代物件縮減為單個值。例如,reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) 計算 ((((1+2)+3)+4)+5)。左側引數 x 是累積值,右側引數 y 是來自 iterable 的更新值。如果存在可選的 initial,它將在計算中放置在可迭代物件的項之前,並在可迭代物件為空時作為預設值。如果未提供 initialiterable 僅包含一項,則返回第一項。

大致等同於

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()

@functools.singledispatch

將函式轉換為 單分派 泛型函式

要定義泛型函式,請使用 @singledispatch 裝飾器對其進行裝飾。當使用 @singledispatch 定義函式時,請注意分派發生在第一個引數的型別上

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

對於在集合型別(例如 list)上分派,但希望型別提示集合的項(例如 list[int])的程式碼,應將分派型別顯式傳遞給裝飾器本身,並將型別提示放入函式定義中

>>> @fun.register(list)
... def _(arg: list[int], verbose=False):
...     if verbose:
...         print("Enumerate this:")
...     for i, elem in enumerate(arg):
...         print(i, elem)

注意

在執行時,該函式將分派到列表的例項,而不管列表中包含的型別如何,即 [1,2,3] 將與 ["foo", "bar", "baz"] 以相同的方式分派。此示例中提供的註解僅用於靜態型別檢查器,沒有執行時影響。

要啟用註冊 lambda 和預先存在的函式,也可以使用函式形式的 register() 屬性

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

register() 屬性返回未裝飾的函式。這使得可以進行裝飾器堆疊、pickle 以及為每個變體獨立建立單元測試

>>> @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

如果特定型別沒有註冊的實現,則會使用其方法解析順序來查詢更通用的實現。使用 @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 支援與其他裝飾器(例如 @classmethod)巢狀。請注意,為了允許 dispatcher.registersingledispatchmethod 必須是最外層的裝飾器。以下是 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__(文件字串))和 WRAPPER_UPDATES(它更新包裝器函式的 __dict__,即例項字典)。

為了允許訪問原始函式以進行內省和其他目的(例如,繞過諸如 lru_cache() 之類的快取裝飾器),此函式會自動向包裝器新增一個 __wrapped__ 屬性,該屬性引用被包裝的函式。

此函式的主要用途是在 裝飾器 函式中,這些函式包裝被裝飾的函式並返回包裝器。如果包裝器函式未更新,則返回函式的元資料將反映包裝器定義而不是原始函式定義,這通常不太有幫助。

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'

如果不使用此裝飾器工廠,示例函式的名稱將是 'wrapper',並且原始的 example() 的文件字串將會丟失。

partial 物件

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

partial.func

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

partial.args

最左邊的位置引數,這些引數將新增到提供給 partial 物件呼叫的位置引數之前。

partial.keywords

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

partial 物件類似於 函式物件,它們都是可呼叫的,弱可引用的,並且可以有屬性。但也有一些重要的區別。例如,__name____doc__ 屬性不會自動建立。此外,在類中定義的 partial 物件行為類似於靜態方法,並且在例項屬性查詢期間不會轉換為繫結方法。