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,實現通常會將它們視為等效呼叫,並且只快取一個結果。(某些型別,如 strint,即使 typed 為 false,也可能被單獨快取。)

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

被包裝的函式配備了一個 cache_parameters() 函式,該函式返回一個新的 dict,顯示 maxsizetyped 的值。這僅供參考。改變這些值沒有效果。

為了幫助衡量快取的有效性並調整 maxsize 引數,被包裝的函式配備了一個 cache_info() 函式,該函式返回一個命名元組,顯示 hits(命中)、misses(未命中)、maxsizecurrsize(當前大小)。

該裝飾器還提供了一個 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.Placeholder

一個單例物件,用作哨兵,在呼叫 partial()partialmethod() 時為位置引數保留一個位置。

在 3.14 版本加入。

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

返回一個新的 偏函式物件,當它被呼叫時,其行為就像 func 被位置引數 args 和關鍵字引數 keywords 呼叫一樣。如果向該呼叫提供了更多引數,它們將被附加到 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

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

>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18

如果 args 中存在 Placeholder 哨兵,當呼叫 partial() 時,它們將首先被填充。這使得可以用 partial() 的呼叫來預填充任何位置引數;如果沒有 Placeholder,只能預填充選定數量的頭部位置引數。

如果存在任何 Placeholder 哨兵,則在呼叫時必須全部填充:

>>> say_to_world = partial(print, Placeholder, Placeholder, "world!")
>>> say_to_world('Hello', 'dear')
Hello dear world!

呼叫 say_to_world('Hello') 會引發一個 TypeError,因為只提供了一個位置引數,但有兩個佔位符必須被填充。

如果 partial() 應用於一個已有的 partial() 物件,輸入物件的 Placeholder 哨兵將用新的位置引數填充。可以透過在先前 Placeholder 佔據的位置插入一個新的 Placeholder 哨兵來保留一個佔位符:

>>> 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 不能作為關鍵字引數傳遞給 partial()

在 3.14 版本發生變更: 添加了對位置引數中 Placeholder 的支援。

class functools.partialmethod(func, /, *args, **keywords)

返回一個新的 partialmethod 描述符,其行為類似於 partial,但它被設計為用作方法定義,而不是直接可呼叫。

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

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

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

在 3.14 版本發生變更: initial 現在支援作為關鍵字引數。

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

也可以使用 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)
...

對於在集合型別(例如,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() 屬性現在支援將 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 支援與其他裝飾器(如 @classmethod)巢狀。請注意,為了允許 dispatcher.registersingledispatchmethod 必須是最外層的裝飾器。下面是將 neg 方法繫結到類而不是類例項的 Negator 類:

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@~abc.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__ 屬性不會自動建立。