Skip to content

functools —— 高阶函数,以及可调用对象上的操作

functools 模块提供了对函数的高阶操作工具,特别适合在函数式编程中简化代码。它通过一些装饰器和工具函数,使得我们可以更加高效地处理函数调用、缓存、比较等任务。

官方文档:https://docs.python.org/zh-cn/3.13/library/functools.html

函数结果缓存

python
@functools.cache(user_function)

简单轻量级未绑定函数缓存。返回值与 lru_cache(maxsize=None) 相同,创建一个查找函数参数的字典的简单包装器。 因为它不需要清除旧值,所以比带有大小限制的 lru_cache() 更小更快。

例:

python
@cache
def factorial(n):
    return n * factorial(n-1) if n else 1

>>> factorial(10)      # 不预先缓存结果,执行 11 次递归调用
3628800
>>> factorial(5)       # 只查找缓存结果值
120
>>> factorial(12)      # 执行两次新的递归调用,另外 10 次已缓存
479001600

该缓存是线程安全的因此被包装的函数可在多线程中使用。 这意味着下层的数据结构将在并发更新期间保持一致性。

如果另一个线程在初始调用完成并被缓存之前执行了额外的调用则被包装的函数可能会被多次调用。

python
@functools.lru_cache(user_function)
@functools.lru_cache(maxsize=128, typed=False)

一个为函数提供缓存功能的装饰器,缓存 maxsize 组传入参数,在下次以相同参数调用时直接返回上一次的结果。用以节约高开销或I/O函数的调用时间。

该缓存是线程安全的因此被包装的函数可在多线程中使用。 这意味着下层的数据结构将在并发更新期间保持一致性。

python
@lru_cache
def count_vowels(sentence):
    return sum(sentence.count(vowel) for vowel in 'AEIOUaeiou')

如果 maxsize 设为 None,LRU 特性将被禁用且缓存可无限增长。

如果 typed 被设置为 true ,不同类型的函数参数将被分别缓存。 如果 typed 为 false ,实现通常会将它们视为等价的调用,只缓存一个结果。(有些类型,如 str 和 int ,即使 typed 为 false ,也可能被分开缓存)。

LRU (least recently used) 缓存 在最近的调用是即将到来的调用的最佳预测值时性能最好 (例如,新闻服务器上的最热门文章倾向于每天发生变化)。 缓存的大小限制可确保缓存不会在长期运行的进程如 web 服务器上无限制地增长。

一般来说,LRU 缓存只应在你需要重复使用先前计算的值时使用。 因此,缓存有附带影响的函数、每次调用都需要创建不同的可变对象的函数(如生成器和异步函数)或不纯的函数如 time() 或 random() 等是没有意义的。

静态 Web 内容的 LRU 缓存示例:

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

以下是使用缓存通过动态规划计算斐波那契数列的例子。

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

对于使用了 @lru_cache 的函数,可以通过 cache_clear() 清除缓存。这在某些场景下非常有用,尤其是当缓存的内容过多或者需要强制更新缓存时。

python
from functools import lru_cache

@lru_cache(maxsize=3)
def expensive_function(n):
    print(f"Calculating {n}...")
    return n * n

expensive_function(4)
expensive_function(5)
expensive_function(6)

# 清除缓存
expensive_function.cache_clear()
python
@functools.cached_property(func)

cached_property 是一个属性装饰器,用于将一个方法变成一个只计算一次并缓存其结果的属性。这对于那些计算比较昂贵的方法,或者你不想每次访问都重新计算的场景非常有用。

python
from functools import cached_property

class MyClass:
    def __init__(self, x):
        self.x = x

    @cached_property
    def square(self):
        print("Calculating square...")
        return self.x ** 2

obj = MyClass(4)
print(obj.square)  # 输出:Calculating square... 16
print(obj.square)  # 不会再计算,直接返回缓存值:16

偏函数

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

用于创建一个新的函数,固定原函数的部分参数。常用于简化函数调用,避免重复传递相同的参数。

python
from functools import partial

def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)  # 创建一个新的函数,固定了 exponent 为 2
print(square(3))  # 输出 9
python
functools.reduce(function, iterable, [initial, ]/)

遍历可迭代对象 iterable 并将它的元素依次通过函数 function 计算,然后累加起来。例如,reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) 就是计算 ((((1+2)+3)+4)+5)

python
from functools import reduce

# 求 1 到 5 的累积乘积
result = reduce(lambda x, y: x * y, [1, 2, 3, 4, 5])
print(result)  # 输出 120
python
class functools.partialmethod(func, /, *args, **keywords)

类似于 functools.partial,但它是为类的方法设计的,可以通过 partialmethod 创建类方法的偏函数。partialmethod 通常用于简化类中的方法调用。

python
from functools import partialmethod

class MyClass:
    def __init__(self, value):
        self.value = value

    def multiply(self, a, b):
        return a * b

    multiply_by_2 = partialmethod(multiply, 2)

obj = MyClass(3)
print(obj.multiply_by_2(5))  # 输出 10

保留被装饰函数的元数据

python
@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

它用于创建一个装饰器时,保持被装饰函数的元数据(如函数名、文档字符串、函数参数等)。当你编写装饰器时,通常需要使用 wraps 来确保被装饰的函数在装饰后依然保有原函数的属性。

python
from functools import wraps

def my_decorator(func):
    @wraps(func)  # 保留原函数的元数据
    def wrapper(*args, **kwargs):
        print("Before the function call")
        result = func(*args, **kwargs)
        print("After the function call")
        return result
    return wrapper

@my_decorator
def greet(name):
    """Greet a person by name."""
    return f"Hello, {name}!"

print(greet.__name__)  # 输出 greet,确保保留了原函数的名称
print(greet.__doc__)   # 输出 Greet a person by name,确保保留了原函数的文档字符串

使用 wraps 装饰器,我们确保 greet 函数的 __name____doc__ 属性保持不变,避免了装饰器带来的元数据丢失。

将函数转换为单分派

单分派 的术语解释

python
@functools.singledispatch

将一个函数转换为单分派。它允许根据函数的第一个参数类型选择不同的实现。

python
from functools import singledispatch

@singledispatch
def print_value(value):
    # 默认的处理
    print(f"Unknown type: {value}")

# 为 int 类型注册方法
@print_value.register(int)
def _(value: int):
    print(f"Integer: {value}")

# 为 str 类型注册方法
@print_value.register(str)
def _(value: str):
    print(f"String: {value}")

# 为 list 类型注册方法
@print_value.register(list)
def _(value: list):
    print(f"List: {value}")

# 测试不同类型的参数
print_value(42)        # 输出 Integer: 42
print_value("Hello")   # 输出 String: Hello
print_value([1, 2, 3]) # 输出 List: [1, 2, 3]
print_value(3.14)      # 输出 Unknown type: 3.14

除此之外,有对于类的方法实现的单分派装饰器。

python
class functools.singledispatchmethod(func)

当你想要根据方法的第一个参数的类型来动态选择执行不同的行为时,可以使用 singledispatchmethod。这种方法通常用于处理多态,使得同一方法可以处理不同类型的参数。

python
from functools import singledispatchmethod

class Printer:
    
    @singledispatchmethod
    def print_value(self, value):
        # 默认的实现
        print(f"Unknown type: {value}")

    @print_value.register(int)  # 为 int 类型注册的方法
    def _(self, value: int):
        print(f"Integer: {value}")

    @print_value.register(str)  # 为 str 类型注册的方法
    def _(self, value: str):
        print(f"String: {value}")

    @print_value.register(list)  # 为 list 类型注册的方法
    def _(self, value: list):
        print(f"List: {value}")


printer = Printer()
printer.print_value(42)        # 输出 Integer: 42
printer.print_value("Hello")   # 输出 String: Hello
printer.print_value([1, 2, 3]) # 输出 List: [1, 2, 3]
printer.print_value(3.14)      # 输出 Unknown type: 3.14

简化类的比较定义

total_ordering 装饰器的主要目的是减少在类中定义比较方法的工作量。通常,为了实现完整的比较功能,类需要实现多个魔法方法(如 __lt____le____gt____ge__ 等)。使用 total_ordering,只需定义两个或更多的基础比较方法,装饰器会自动推导出其他比较方法。

python
@functools.total_ordering

total_ordering 装饰器接受一个类,并要求该类至少定义两个比较操作:

  • __eq__:相等比较。
  • __lt__:小于比较。
python
from functools import total_ordering

@total_ordering
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

    def __lt__(self, other):
        return (self.x, self.y) < (other.x, other.y)

# 创建一些实例
p1 = Point(1, 2)
p2 = Point(2, 3)
p3 = Point(1, 2)

# 比较操作
print(p1 == p3)  # True,调用 __eq__
print(p1 < p2)   # True,调用 __lt__
print(p1 <= p3)  # True,自动生成的 __le__
print(p2 > p1)   # True,自动生成的 __gt__
print(p2 >= p3)  # True,自动生成的 __ge__

为方便开发而创建的常用库指南