💻 Python functools

介绍

python内置库。

functools库是Python 3.5及以上版本引入的,用于支持函数式编程,旨在提高代码的可读性和可维护性,尤其在大型项目中非常有用。

常用函数

__all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES',
           'total_ordering', 'cache', 'cmp_to_key', 'lru_cache', 'reduce',
           'partial', 'partialmethod', 'singledispatch', 'singledispatchmethod',
           'cached_property']

functools.cache / @cache

functools.cache(user_function)
在Python 3.9中,functools 模块引入了一个新的装饰器 @cache,用于缓存函数的返回值,这可以显著提高函数的性能,特别是对于那些计算密集型或经常被重复调用的函数。
@cachefunctools.cache 的简写形式,与 @lru_cache 类似,只不过它没有大小限制,即缓存的所有结果都会一直保留,直到程序结束或缓存被手动清除。

  • 适用场景
    1. 计算密集型函数:如递归计算、复杂数学计算等。
    2. 不变函数:函数的返回值只取决于输入参数,且输入参数相同时返回值总是相同。
    3. 频繁调用:函数在程序运行期间被频繁调用,且输入参数有重复。
  • 注意事项
    1. 内存消耗:由于 @cache 没有缓存大小限制,如果被缓存的函数有大量不同的输入参数,可能会导致内存使用过多。
    2. 不适合变动函数:如果函数的返回值依赖于外部状态或参数的变化,不适合使用 @cache

如下demo,fibonacci 函数使用了 @cache 装饰器,因此它的返回值会被缓存。如果多次调用 fibonacci 函数,且参数相同,函数不会重新计算,而是直接返回缓存中的结果。

from functools import cache

@cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # 输出 55
print(fibonacci(20))  # 输出 6765

清除缓存:缓存可以通过调用装饰函数的 cache_clear 方法来手动清除:

fibonacci.cache_clear()

functools.lru_cache / @lru_cache

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

functools.lru_cache 是 Python 中用于缓存函数结果的装饰器,利用了最近最少使用(LRU,Least Recently Used)缓存策略。它在处理重复计算、递归函数或需要优化性能的场景中特别有用。

  • 主要参数
    • maxsize: 缓存的最大条目数。如果设置为 None,缓存大小无限制。如果缓存满了,最久未使用的条目会被移除。
    • typed: 如果设置为 True,不同类型的参数将被分别缓存。例如,f(3)f(3.0) 会被视为不同的调用并分别缓存。
  • 适用场景
    1. 计算密集型函数:如递归计算、复杂数学计算等。
    2. 不变函数:函数的返回值只取决于输入参数,且输入参数相同时返回值总是相同。
    3. 频繁调用:函数在程序运行期间被频繁调用,且输入参数有重复。
  • 注意事项
    1. 内存消耗:缓存大小受 maxsize 限制,如果设置为 None 可能会导致内存使用过多。
    2. 不适合变动函数:如果函数的返回值依赖于外部状态或参数的变化,不适合使用 @lru_cache

比较 @cache@lru_cache

@cache@lru_cache 都是用于缓存的装饰器,但有一些不同点:

  • @cache 没有缓存大小限制,适合用于确定性函数(即总是返回相同结果的函数)。
  • @lru_cache 允许指定缓存大小,使用最近最少使用(LRU)算法管理缓存,适合用于缓存大小需要受控的场景。

如下demo,fibonacci 函数使用了 @lru_cache 装饰器,并指定了 maxsize=128,表示最多缓存 128 个结果。超过这个数量的缓存将按照 LRU 策略进行管理。

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(10))  # 输出 55
print(fibonacci(20))  # 输出 6765

带参数的用法
如果你的函数接受不同类型的参数,并且你希望将它们分别缓存,可以使用 typed=True
如下demo,example_function(3)example_function(3.0) 会被视为不同的调用并分别缓存。

@lru_cache(maxsize=128, typed=True)
def example_function(x):
    return x

print(example_function(3))   # 调用 example_function(3),缓存结果
print(example_function(3.0)) # 调用 example_function(3.0),缓存结果

清除缓存
可以通过调用装饰函数的 cache_clear 方法来手动清除缓存:

fibonacci.cache_clear()

查看缓存信息
可以通过以下方法查看缓存的命中率和缓存的使用情况:

  • hits 表示缓存命中次数。
  • misses 表示缓存未命中次数。
  • maxsize 表示缓存的最大条目数。
  • currsize 表示当前缓存的条目数。
    print(fibonacci.cache_info())  # 输出 CacheInfo(hits=9, misses=21, maxsize=128, currsize=22)
    

functools.partial

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

Return a new partial object which when called will behave like func called with the positional arguments args and keyword arguments keywords. 用于创建一个新的可调用对象,该对象在调用时的行为类似于使用位置参数 args 和关键字参数 keywords 调用 func 函数。如果调用时提供了更多的参数,则它们会被附加到 args 上。如果提供了额外的关键字参数,则它们会扩展并覆盖 keywords。 -> 创建一个新的可调用对象,这个对象是原函数的一个部分应用(partial application)。也就是说,partial()可以用来固定原函数的一个或多个参数,从而得到一个新的函数。新函数的调用方式与原函数相同,只不过在调用时可以省略掉原函数被固定的参数。

等价于如下代码:

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

demo:

from functools import partial
def add(x, y):
    return x + y
add_five = partial(add, 5)
print(add_five(3)) # 输出 8

functools.update_wrapper

functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

用于更新包装函数 (wrapper function) 以便其看起来像被包装的函数 (wrapped function)。 它通常与 functools.wraps 一起使用,以确保装饰器函数能够正确地保留被装饰函数的元数据,如名称、模块和文档字符串。

  • 主要参数
    • wrapper: 需要更新的包装函数。
    • wrapped: 被包装的函数。
    • assigned: 一个元组,指定哪些属性需要从被包装函数赋值到包装函数,默认值是 functools.WRAPPER_ASSIGNMENTS,包括 __name__, __module__, 和 __doc__
    • updated: 一个元组,指定哪些属性的字典需要被更新,默认值是 functools.WRAPPER_UPDATES,通常是 __dict__

Demo2: 手动使用 update_wrapper

如果你不使用 functools.wraps,也可以手动调用 update_wrapper: 在这个示例中,functools.update_wrapper(wrapper, func) 手动将 func 的元数据更新到 wrapper 上,从而保留了被包装函数的名称和文档字符串。

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result
    functools.update_wrapper(wrapper, func)     # !!!
    return wrapper
@my_decorator
def say_hello():
    """This function says hello."""
    print("Hello!")
say_hello()
print(say_hello.__name__)  # 输出 'say_hello'
print(say_hello.__doc__)   # 输出 'This function says hello.'

@functools.wraps

@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) functools.wraps 是一个装饰器,常用于定义其他装饰器时,以便保持被装饰函数的元数据。它可以将被装饰函数的属性复制到包装函数中,使得包装函数看起来更像被装饰的原始函数。

代码实现:实际就是调用了 functools.update_wrapper 函数。

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function
       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)
  • 主要参数
    • wrapped: 被包装的函数。
    • assigned: 一个元组,指定哪些属性需要从被包装函数赋值到包装函数,默认值是 functools.WRAPPER_ASSIGNMENTS,包括 __name__, __module__, 和 __doc__
    • updated: 一个元组,指定哪些属性的字典需要被更新,默认值是 functools.WRAPPER_UPDATES,通常是 __dict__

demo1: functools.wraps(func)

在这个示例中,functools.wraps(func) 实际上就是 functools.update_wrapper(wrapper, func) 的简写,它确保了 say_hello 函数的名称和文档字符串被正确地保留在 wrapper 函数中。

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result
    return wrapper
@my_decorator  # 相当于 decorated = my_decorator(decorated)
def say_hello():
    """This function says hello."""
    print("Hello!")
say_hello()
print(say_hello.__name__)  # 输出 'say_hello'
print(say_hello.__doc__)   # 输出 'This function says hello.'

不使用@wraps,则:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result
    return wrapper
@my_decorator
def say_hello():
    """This function says hello."""
    print("Hello!")
say_hello()
print(say_hello.__name__)  # 输出 'wrapper'
print(say_hello.__doc__)   # 输出 'None'

再来一个例子:

import time
from functools import wraps

def timethis(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        r = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end -start)
        return r
    return wrapper

@timethis
def countdown(n: int):
    """count down docstring.."""
    while n > 0:
        n -= 1

print(countdown.__name__)
print(countdown.__doc__)
print(countdown.__annotations__)    # 存储函数参数和返回值的类型注解

# run:
countdown
count down docstring..
{'n': <class 'int'>}