前面一篇中对map()、filter()等高阶函数进行了介绍,在functools模块中python提供了更多的高阶函数。高阶函数是函数式编程的重要基础,高阶函数的理解往往也比较困难。高阶函数的特点在前面已经介绍过了,这里就不再重复。
函数缓存装饰器
@functools.lru_cache
语法定义:
@functools.lru_cache(user_function)
@functools.lru_cache(maxsize=128, typed=False)
一个为函数提供缓存功能的装饰器,在下次以相同参数调用时直接返回上一次的结果。用以节约高开销或I/O函数的调用时间。
from functools import lru_cache@lru_cache()
def add(x, y):print(f"函数被调用运行: {x} + {y}")return x+yprint(add(1,2))
print(add(1, 2))
print(add(x=1, y=2))
print(add(1, 2))‘’'
函数被调用运行: 1 + 2
3
3
函数被调用运行: 1 + 2
3
3
‘''
- 该缓存是线程安全的因此被包装的函数可在多线程中使用。 这意味着下层的数据结构将在并发更新期间保持一致性。如果另一个线程在初始调用完成并被缓存之前执行了额外的调用则被包装的函数可能会被多次调用。
- 由于使用字典来缓存结果,因此传给该函数的位置和关键字参数必须为 hashable。
- 不同的参数模式可能会被视为具有单独缓存项的不同调用。 例如,f(a=1, b=2) 和 f(b=2, a=1) 因其关键字参数顺序不同而可能会具有两个单独的缓存项。
- 如果指定了 user_function,它必须是一个可调用对象。 这允许 lru_cache 装饰器被直接应用于一个用户自定义函数。
- 如果 maxsize 设为 None,LRU 特性将被禁用且缓存可无限增长。
如果 typed 被设置为 true ,不同类型的函数参数将被分别缓存。 如果 typed 为 false ,实现通常会将它们视为等价的调用,只缓存一个结果。(有些类型,如 str 和 int ,即使 typed 为 false ,也可能被分开缓存)。
@lru_cache(maxsize=None)
def fib(n):if n < 2:return nreturn 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)
注意,类型的特殊性只适用于函数的直接参数而不是它们的内容。 标量参数 Decimal(42) 和 Fraction(42) 被视为具有不同结果的不同调用。相比之下,元组参数 ('answer', Decimal(42)) 和 ('answer', Fraction(42)) 被视为等同的。
被包装的函数配有一个 cache_parameters() 函数,该函数返回一个新的 dict 用来显示 maxsize 和 typed 的值。 这只是出于显示信息的目的。 改变值没有任何效果。
为了帮助衡量缓存的有效性以及调整 maxsize 形参,被包装的函数会带有一个 cache_info() 函数,它返回一个 named tuple 以显示 hits, misses, maxsize 和 currsize。
该装饰器也提供了一个用于清理/使缓存失效的函数 cache_clear() 。
原始的未经装饰的函数可以通过 __wrapped__ 属性访问。它可以用于检查、绕过缓存,或使用不同的缓存再次装饰原始函数。
from functools import lru_cache@lru_cache
def add(x, y):print(f"函数被调用运行: {x} + {y}")return x+yprint(add(1, 2))
print(add(1.0,2))print(add.cache_parameters()) #{'maxsize': 128, 'typed': False}
add.cache_clear()
print(add(1, 2))‘’'
函数被调用运行: 1 + 2
3
3
{'maxsize': 128, 'typed': False}
函数被调用运行: 1 + 2
3
‘''
借助“ lru_cache” 装饰器,您几乎可以解决40%到50%的动态编程问题。 LRU代表“最近最少使用”,它会将上次函数执行出来的结果保存在内存中,并且在必须再次执行该函数时,首先进行检查高速缓存,如果找到,它将返回结果,否则将继续执行该函数。
LRU 是 “Least Recently Used” 的缩写,意思是 “最近最少使用”。LRU 缓存就是一种缓存淘汰算法,当缓存达到预设的容量上限时,会优先淘汰最近最少使用的数据。
当我们需要通过函数执行计算量大或受约束的操作时, “ lru_cache” 可以为我们节省大量时间。
缓存会保持对参数的引用并返回值,直到它们结束生命期退出缓存或者直到缓存被清空。
如果一个方法被缓存,则 self 实例参数会被包括在缓存中。 请参阅 我该如何缓存方法调用?
@functools.cache
语法定义:
@functools.cache(user_function)
简单轻量级未绑定函数缓存。 有时称为 "memoize"。
返回值与 lru_cache(maxsize=None) 相同,创建一个查找函数参数的字典的简单包装器。 因为它不需要移出旧值,所以比带有大小限制的 lru_cache() 更小更快。
该缓存是线程安全的因此被包装的函数可在多线程中使用。 这意味着下层的数据结构将在并发更新期间保持一致性。如果另一个线程在初始调用完成并被缓存之前执行了额外的调用则被包装的函数可能会被多次调用。
from functools import cache@cache
def factorial(n):print(f'factorial({n})')return n * factorial(n-1) if n else 1for i in range(10,15):factorial(i)‘’'
factorial(10)
factorial(9)
factorial(8)
factorial(7)
factorial(6)
factorial(5)
factorial(4)
factorial(3)
factorial(2)
factorial(1)
factorial(0)
factorial(11)
factorial(12)
factorial(13)
factorial(14)
‘''
@functools.cached_property
语法定义:
@functools.cached_property(func)
将一个类方法转换为特征属性,一次性计算该特征属性的值,然后将其缓存为实例生命周期内的普通属性。 类似于 property() 但增加了缓存功能。 对于在其他情况下实际不可变的高计算资源消耗的实例特征属性来说该函数非常有用。
from functools import cached_propertyclass Factorial:def __init__(self, n:int):self.num = ndef set(self, n:int):self.num = n@cached_propertydef value(self):def factorial(n):return n * factorial(n - 1) if n else 1print(f'value({self.num})')return factorial(self.num)fac =Factorial(10)
print(fac.value)
print(fac.value)
fac.set(11)
print(fac.value) #仍然没有改变’’’
value(10)
3628800
3628800
3628800
‘’‘
cached_property() 的设定与 property() 有所不同。 常规的 property 会阻止属性写入,除非定义了 setter。 与之相反,cached_property 则允许写入。
cached_property 装饰器仅在执行查找且不存在同名属性时才会运行。 当运行时,cached_property 会写入同名的属性。 后续的属性读取和写入操作会优先于 cached_property 方法,其行为就像普通的属性一样。
缓存的值可通过删除该属性来清空。 这允许 cached_property 方法再次运行。
注意,这个装饰器会影响 PEP 412 键共享字典的操作。 这意味着相应的字典实例可能占用比通常时更多的空间。
而且,这个装饰器要求每个实例上的 __dict__ 是可变的映射。 这意味着它将不适用于某些类型,例如元类(因为类型实例上的 __dict__ 属性是类命名空间的只读代理),以及那些指定了 __slots__ 但未包括 __dict__ 作为所定义的空位之一的类(因为这样的类根本没有提供 __dict__ 属性)。
如果可变的映射不可用或者如果想要节省空间的键共享,可以通过在 lru_cache() 上堆叠 property() 来实现类似 cached_property() 的效果。 请参阅 我该如何缓存方法调用? 了解这与 cached_property() 之间区别的详情。
下面是property装饰器和cached_property装饰器的对比:
from functools import cached_propertyclass Sample():def __init__(self):self.result = 50@property# a method to increase the value of# result by 50def increase(self):self.result = self.result + 50return self.result# obj is an instance of the class sample
obj = Sample()
print(obj.increase)
print(obj.increase)
print(obj.increase)’’’
100
150
200
‘’‘
上面的函数使用了property装饰器,每次调用的时候会重新计算。直接换成cached_property装饰器:
from functools import cached_propertyclass Sample():def __init__(self):self.result = 50@cached_property# a method to increase the value of# result by 50def increase(self):self.result = self.result + 50return self.result# obj is an instance of the class sample
obj = Sample()
print(obj.increase)
print(obj.increase)
print(obj.increase)’’’
100
100
100
‘’‘
属性值第一次计算后被缓存,后续的调用都是使用第一次的值。
比较方法扩展装饰器
@functools.total_ordering
用Python编程通常需要编写自己的类。在某些情况下,你希望能够比较该类的不同实例。根据你想要比较它们的方式,你最终可能会实现像__lt__、__le__、__gt__、 __ge__ 或__eq__ 这样的函数,以便能够使用相应的<、<=、>、>=和==操作符。
使用@total_ordering装饰器,只需要定义一个方法,Python自动扩展其他方法的实现,可以大幅的减轻了指定所有可能的全比较操作的工作。此类必须包含以下方法之一:__lt__()
、__le__()
、__gt__()
或 __ge__()
。另外,此类必须支持 __eq__()
方法。
from functools import total_ordering@total_ordering
class Student:def __init__(self, name, age):self.name = nameself.age = agedef __eq__(self, other):if self.name != other.name:return Falseif self.age != other.age:return Falsereturn Truedef __lt__(self, other):return self.age < other.ages1 = Student('John', 13)
s2 = Student('Alice', 15)
s3 = Student('Chen', 8)
print(f'{s1!=s2}')
print(f'{s1<s2}')
print(f'{s3>s2}')
print(f'{s1>=s3}’)’’’
True
True
False
True
‘’‘
事实上Python默认会给自定义对象都加上比较的这一套方法(__lt__、__le__等),但实际在使用的时候不能使用。
from functools import total_ordering# @total_ordering
class Student:def __init__(self, name, age):self.name = nameself.age = age# def __eq__(self, other):# if self.name != other.name:# return False## if self.age != other.age:# return False## return True## def __lt__(self, other):# return self.age < other.ages1 = Student('John', 13)
s2 = Student('Alice', 15)
s3 = Student('Chen', 8)
print(hasattr(s1, '__ge__')) #True
print(f'{s1>=s3}') #TypeError: '>=' not supported between instances of 'Student' and ‘Student'
虽然此装饰器使得创建具有良好行为的完全有序类型变得非常容易,但它 确实 是以执行速度更缓慢和派生比较方法的堆栈回溯更复杂为代价的。 如果性能基准测试表明这是特定应用的瓶颈所在,则改为实现全部六个富比较方法应该会轻松提升速度。
这个装饰器不会尝试重载类 或其上级类 中已经被声明的方法。 这意味着如果某个上级类定义了比较运算符,则 total_ordering 将不会再次实现它,即使原方法是抽象方法。
函数重载装饰器
@functools.singledispatch
Python允许定义重载函数,但Python没有强类型,不能像C++、JAVA等强类型语言一样直接依赖参数类型来适配不同的重载函数,专门提供了一套机制,其中装饰器 @singledispatch
就是来定义这种重载函数的,需要注意的是,只能依据第一个参数的类型来区分重载函数。
from functools import singledispatch
@singledispatch
def fun(arg, verbose=False):if verbose:print("Let me just say,", end=" ")print(arg)
要将重载的实现添加到函数中,请使用泛型函数的 register()
属性(使用@singledispatch装饰过的函数对象,如本例子中为fun,则使用fun.register(),如果是foo()函数对象,就使用foo.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.UnionType 和 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)
要启用注册 lambda 和现有的函数,也可以使用 register() 属性的函数形式:
def nothing(arg, verbose=False):print("Nothing.")fun.register(type(None), nothing)
register() 属性会返回未被装饰的函数。 这将启用装饰器栈、封存,并为每个变量单独创建单元测试:
@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 类型进行注册,这意味着它将在找不到更好的实现时被使用。
如果一个实现被注册到 abstract base class,则基类的虚拟子类将被发送到该实现:
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>
@functools.singledispatchmethod
class functools.singledispatchmethod(func)
这事方法的重载函数装饰器,用法与singledispatch相同,只是它是用来装饰对象方法的。
当使用 @singledispatchmethod 定义一个函数时,请注意发送操作将针对第一个非 self 或非 cls 参数的类型上:
class Negator:@singledispatchmethoddef neg(self, arg):raise NotImplementedError("Cannot negate a")@neg.registerdef _(self, arg: int):return -arg@neg.registerdef _(self, arg: bool):return not arg
@singledispatchmethod 支持与其他装饰器如 @classmethod 相嵌套。 请注意为了允许 dispatcher.register,singledispatchmethod 必须是 最外层的 装饰器。 下面是一个 Negator 类包含绑定到类的 neg 方法,而不是一个类实例:
class Negator:@singledispatchmethod@classmethoddef neg(cls, arg):raise NotImplementedError("Cannot negate a")@neg.register@classmethoddef _(cls, arg: int):return -arg@neg.register@classmethoddef _(cls, arg: bool):return not arg
同样的模式也可被用于其他类似的装饰器: @staticmethod, @abstractmethod 等等。
包装器装饰器
@functools.wraps
@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'
如果不使用这个装饰器工厂函数,则 example 函数的名称将变为 'wrapper'
,并且 example()
原本的文档字符串将会丢失。
@functools.update_wrapper
functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
更新一个 wrapper 函数以使其类似于 wrapped 函数。 可选参数为指明原函数的哪些属性要直接被赋值给 wrapper 函数的匹配属性的元组,并且这些 wrapper 函数的属性将使用原函数的对应属性来更新。 这些参数的默认值是模块级常量 WRAPPER_ASSIGNMENTS (它将被赋值给 wrapper 函数的 __module__, __name__, __qualname__, __annotations__ 和 __doc__ 即文档字符串) 以及 WRAPPER_UPDATES (它将更新 wrapper 函数的 __dict__ 即实例字典)。
为了允许出于内省和其他目的访问原始函数(例如绕过 lru_cache() 之类的缓存装饰器),此函数会自动为 wrapper 添加一个指向被包装函数的 __wrapped__ 属性。
此函数的主要目的是在 decorator 函数中用来包装被装饰的函数并返回包装器。 如果包装器函数未被更新,则被返回函数的元数据将反映包装器定义而不是原始函数定义,这通常没有什么用处。
update_wrapper() 可以与函数之外的可调用对象一同使用。 在 assigned 或 updated 中命名的任何属性如果不存在于被包装对象则会被忽略(即该函数将不会尝试在包装器函数上设置它们)。 如果包装器函数自身缺少在 updated 中命名的任何属性则仍将引发 AttributeError。