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.pythonlang.cn/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__ 属性不会自动创建。