目录
八、functools.cached_property ( >= Python 3.8)
一、引言
Python是一门多功能语言,多功能性体现在它具有很多功能强大的内置模块,这些模块使用我们在写代码时不用重复造轮子。functools模块就是这样一个典型的模块。充分利用它可以让Python代码更整洁、清晰和专业。
二、funtools.partial
Partial这个功能的目的就是为了在重复使用有多个参数的且部分参数固定的函数时提供辅助。比如,int()
函数可以用于将字符串转换为整数。如果待转换的字符串是一个二进制整数的话,我们必须添加第二个参数表示转换前的进制。如果int()函数反复使用,则第二个参数需要重复写入,因此为了省略这个参数,可以在一处实现多处使用:
print(f"The binary integer 101101 is equal: {int('11110111', base=2)}")
# The binary integer 101101 is equal: 247
from functools import partial
binary_trans = partial(int, base=2)
print(f"101101 (base 2) is equal to: {binary_trans('101101')} (base 10)")
print(f"1111110 (base 2) is equal to: {binary_trans('1111110')} (base 10)")
print(f"11111111 (base 2) is equal to: {binary_trans('11111111')} (base 10)")
101101 (base 2) is equal to: 45 (base 10)
1111110 (base 2) is equal to: 126 (base 10)
11111111 (base 2) is equal to: 255 (base 10)
对于自定义函数,也同样可以使用partial方法。
from functools import partial
def multiply(x, y):
print(x)
return x * y
double = partial(multiply, 2)
print(double(5)) # 输出 10
输出:
2
10
#说明partial固定的参数是第一个参数;
三、functools.lru_cache
使用最近最少使用(Least Recently Used, LRU)缓存装饰器,提高函数的性能,尤其适用于计算密集型或 I/O 密集型函数。
from functools import lru_cache
import timeit
def fibonacci_norm(n):
if n < 2:
return n
return fibonacci_norm(n - 1) + fibonacci_norm(n - 2)
@lru_cache(maxsize=32)
def fibonacci_lru_cached(n):
if n < 2:
return n
return fibonacci_lru_cached(n - 1) + fibonacci_lru_cached(n - 2)
def timed_fibonacci_norm():
resutl = fibonacci_norm(30)
return resutl
def timed_fibonacci_cached():
resutl = fibonacci_lru_cached(30)
# 清除缓存结果
fibonacci_lru_cached.cache_clear()
return resutl
if __name__ == '__main__':
time_norm = timeit.timeit(timed_fibonacci_norm, number=50) #number= 50表示执行50次,再结果中计算平均每次执行时间;
time_cached = timeit.timeit(timed_fibonacci_cached, number=50)
print(f'Average time for fibonacci (norm): {time_norm / 50} s')
print(f'Average time for fibonacci (cached): {time_cached / 50} s')
print(f'Cached version is faster than norm: {time_norm / time_cached / 50}')
结果:
Average time for fibonacci (norm): 0.26051544800000104 s
Average time for fibonacci (cached): 8.665999998811458e-06 s
Cached version is faster than norm: 601.235744370484
从上面的结果可以看出,相较于常规方法,使用 lru_cache
方法速度快了大约 601倍,对性能的提升非常显著。而且随着计算数字的增大,这个差距还会继续扩大。
此外,缓存方法还有另一种实现方式,即 cache
装饰器。其实现方式与 lru_cache
类似。我们可以看看它与常规方法的性能差距:
from functools import cache
import timeit
@cache
def fibonacci_cached(n):
if n < 2:
return n
return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)
def timed_fibonacci_cached():
resutl = fibonacci_lru_cached(30)
# 重新装饰函数以清楚缓存
fibonacci_cached = cache(fibonacci_lru_cached)
return resutl
if __name__ == '__main__':
time_norm = timeit.timeit(timed_fibonacci_norm, number=50)
time_cached = timeit.timeit(timed_fibonacci_cached, number=50)
print(f'Average time for fibonacci (norm): {time_norm / 50} s')
print(f'Average time for fibonacci (cached): {time_cached / 50} s')
print(f'Cached version is faster than norm: {time_norm / time_cached / 50}')
结果:
Average time for fibonacci (norm): 0.2594117379999989 s
Average time for fibonacci (cached): 3.005999997185427e-06 s
Cached version is faster than norm: 1725.959668948043
可以看出,cache
的性能更好,比常规方法快了约 1725 倍,主要是因为 lru_cache
存在缓存策略,所以在缓存管理上会有一定开销。
lru_cache
和 cache
各有其优点和适用场景,性能上的差异主要取决于具体的使用场景和缓存策略的需求。下面是一些对比和选择建议:
lru_cache
的特点:
-
LRU 缓存策略:
lru_cache
使用最近最少使用(Least Recently Used, LRU)策略,当缓存达到最大容量时,会自动清除最久未使用的条目。这对于需要限制缓存大小并且期望自动管理缓存淘汰的场景非常有用。 -
缓存统计信息:
lru_cache
提供缓存命中率等统计信息(通过cache_info
方法),有助于监控和优化缓存使用。 -
性能:在缓存大小有限且需要频繁访问缓存条目的情况下,
lru_cache
能够显著提升性能,但在缓存管理上会有一定的开销。
cache
的特点:
-
无缓存策略:
cache
是一个简单的无策略缓存,它没有缓存淘汰机制,即缓存条目会一直保留,直到程序终止或显式清除。这适合于需要缓存所有结果且不需要考虑内存限制的场景。 -
性能:
cache
的性能开销较小,因为它没有管理缓存条目的开销,在没有内存限制和缓存淘汰需求的情况下,cache
可以提供更好的性能。
性能比较与选择:
-
内存限制:如果你的应用需要限制内存使用,并且缓存数据量可能很大,选择
lru_cache
更为合适,因为它能够自动管理缓存大小并清除旧条目。 -
缓存条目数量少:如果缓存的数据量较少,且不会超出内存限制,使用
cache
会更加简单高效,因为它没有缓存管理的开销。 -
缓存管理需求:如果你需要了解缓存的使用情况和命中率,
lru_cache
提供的统计信息会很有帮助。 -
性能测试:对于特定应用场景,可以通过实际测试来比较两者的性能。可以使用
timeit
模块进行多次调用的时间测量,评估两者在实际应用中的表现。
四、functools.reduce
functools.reduce
是 Python 很重要的高阶函数之一,它用于对可迭代对象中的元素进行累计操作,最终将其简化为单一的值。reduce
可以说是一个“归约”函数,通过对序列中的元素依次应用指定的二元操作,将序列归约为一个值。
reduce
的作用是对序列进行二元操作,并将序列简化为一个单一的值。它的使用格式如下:
from functools import reduce
result = reduce(function, iterable[, initializer])
# function:一个接受两个参数的函数,reduce 会将其应用于 iterable 的元素。
# iterable:一个可迭代对象(如列表、元组等)。
# initializer(可选):初始值,如果提供,则首先将其与序列的第一个元素一起传递给 function。
工作原理:reduce
从 iterable
中取前2个元素,将其传递给 function
,得到的结果与第三个元素一起再次传递给 function
,如此重复,直到序列处理完毕,最终得到一个单一的值。
示例:
#计算最大值:
from functools import reduce
numbers = [11, 25, 397, 40, 55]
result = reduce(lambda x, y: max(x,y), numbers)
print(result) # 输出 397
在这个例子中,reduce
使用了一个匿名函数 lambda x, y: max(x,y)
,该函数接受两个参数并返回它们的max。reduce
会依次将序列中的元素求最大,最终得到 397。
initializer
的使用:
如果提供了 initializer
,则首先将其与 iterable
的第一个元素一起传递给 function
。在上例中,如果result = reduce(lambda x, y: max(x,y), numbers,555),initializer= 555,即最初传入555,然后再取序列的第一个元素参与运算,
则最后结果最大值为555。
应用场景:
-
累计计算:例如求和、求积、最大值、最小值等。
-
序列转换:例如将二进制数字序列转换为整数。
-
函数组合:将多个函数组合成一个单一的函数。
-
数据聚合:在数据处理和分析中,用于对数据进行聚合操作。
示例:假设我们有一个包含产品销售数据的列表,每个元素是一个字典,包含产品类别和销售额。我们希望计算每个类别的平均销售额。
from functools import reduce
sales_data = [
{'category': 'A', 'amount': 200},
{'category': 'B', 'amount': 300},
{'category': 'A', 'amount': 350},
{'category': 'B', 'amount': 600},
{'category': 'C', 'amount': 450}
]
# 定义一个函数,用于合并两个数据项,将item项合并到aggregated中,如果aggregated没有包含item
#则新建,返回每两个item合并后的数据项aggregated
def merge_sales(aggregated, item):
category = item['category']
amount = item['amount']
if category in aggregated:
aggregated[category]['total'] += amount
aggregated[category]['count'] += 1
else:
aggregated[category] = {'total': amount, 'count': 1}
return aggregated
# 使用 reduce 对数据进行分组聚合
aggregated_sales = reduce(merge_sales, sales_data, {})
# 计算平均销售额
average_sales = {category: data['total'] / data['count'] for category, data in aggregated_sales.items()}
print(average_sales)
{'A': 275.0, 'B': 450.0, 'C': 450.0}
functools.reduce
是一个功能强大的工具,适用于需要对序列进行累计或归约操作的场景。它可以通过二元操作将序列中的元素逐步简化为一个单一的值,对于某些复杂的数据处理和聚合操作非常有用。然而,由于其使用较为复杂,对于某些简单的需求,sum
、min
、max
等内置函数可能更加直观和高效。
五、functools.update_wrapper
update_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
return wrapper
@my_decorator
def say_hello():
"""This is a greet function."""
print('Hello!')
say_hello()
print(say_hello.__name__)
print(say_hello.__doc__)
结果:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
wrapper
None
从装饰器函数的内在逻辑上说,结果是没错的, sayh_hello()已经变成了wrapper()所以执行wrapper ,而wrapper函数名为wrapper,wrapper定义中没有 说明,所以返回None。
但是对于用户来说,__name__
和 __doc__
似乎并没有正确打印 say_hello()
的元数据信息(装饰器实际只是扩展了函数的功能,say_hello()的基本信息应还是在原函数上
)。在生产系统中,这会导致严重的 bug,因为如果连正确的名称都无法获取,就可能会调用错误的函数。
这就是使用装饰器的副作用。你不仅需要“包装”函数,还需要包装其元数据,如名称、文档等。
functools.update_wrapper
就可以达到包装函数的同时,并且复制被包装函数的元数据信息。update_wrapper
主要用于将原始函数的属性复制到包装器函数上,包括以下属性:
-
__module__: 模块名
-
__name__: 函数名
-
__qualname__: 函数的限定名
-
__annotations__: 函数的注解
-
__doc__: 函数的文档字符串
这时,在my_decorator外壳中,调用update_wrapper(wrapper, func) 就可以将被装饰函数say_hello()
的属性拷贝到wrapper函数,从而实现元数据信息更新。
from functools import update_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
update_wrapper(wrapper, func)
return wrapper
@my_decorator
def say_hello():
"""This is a greet function."""
print('Hello!')
say_hello()
print(say_hello.__name__)
print(say_hello.__doc__)
结果:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
say_hello
This is a greet function.
应用场景:
functools.update_wrapper
在以下场景中非常有用:
-
开发自定义装饰器:当你开发自定义装饰器时,如果不使用
update_wrapper
或functools.wraps
,包装器函数将丢失原始函数的重要元数据。这样可能会影响调试、文档生成等工具的使用。 -
保持代码可读性:保持装饰器包装后的函数名称和文档字符串不变,有助于代码的可读性和维护性。即使函数被装饰后,开发者仍然可以通过查看函数的名称和文档字符串了解其功能。
-
调试和日志记录:在调试和日志记录中,保持函数的元数据可以提供准确的函数调用信息,有助于定位问题。
六、functools.total_ordering
functools.total_ordering
用于简化实现自定义类的全部比较操作。通常,如果你想让一个类支持所有的比较操作(如 <
、<=
、>
、>=
),需要实现这些操作的所有方法。而 total_ordering
则可以用于减少代码量,你只需实现部分方法(__eq__
和一个其他的比较方法),其余的比较方法 total_ordering
会自动生成。
functools.total_ordering
的作用:total_ordering
的作用是通过实现类的一部分比较方法(至少 __eq__
和一个其他的比较方法,如 __lt__
、__le__
、__gt__
或 __ge__
),自动生成其余的比较方法,从而简化比较方法的实现。下面是一个示例,展示如何使用 total_ordering
简化类的比较操作实现:
from functools import total_ordering
@total_ordering
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __lt__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.age < other.age
# 现在 Person 类自动支持所有比较操作
alice = Person("Alice", 30)
bob = Person("Bob", 25)
print(alice > bob) # 输出 True
print(alice >= bob) # 输出 True
print(alice < bob) # 输出 False
print(alice <= bob) # 输出 False
结果:
True
True
False
False
如上所示,最小只要定义了> 、<、>=、<=中的一个 就可以自动生成其他比较方法。
具体来说,total_ordering
按如下规则生成缺失的方法:
-
如果定义了
__lt__
和__eq__
:-
__le__
被定义为lambda self, other: self.__lt__(other) or self.__eq__(other)
-
__gt__
被定义为lambda self, other: not self.__lt__(other) and not self.__eq__(other)
-
__ge__
被定义为lambda self, other: not self.__lt__(other)
-
-
可见只要定义了
__lt__
和__eq__ 就可以用布尔代数生成其他方法,其他方法类似;
应用场景:
-
简化代码实现:当你需要为自定义类实现所有比较操作时,只需实现最少的方法,剩余的方法由
total_ordering
自动生成,减少了重复代码。 -
提高代码可维护性:通过减少重复实现比较方法,可以使代码更简洁、更易于维护。
-
确保一致性:通过自动生成的方法,确保了比较操作的一致性,避免手动实现时可能出现的错误。
七、functools.singledispatch
functools.singledispatch
是 Python 标准库 functools
模块中的一个装饰器,用于实现单分派泛型函数。单分派泛型函数根据第一个参数的类型进行分派,不同类型的参数会调用不同的函数实现。这在需要根据输入参数的类型执行不同逻辑的场景中非常有用。
functools.singledispatch
的作用:singledispatch
使得你可以定义一个通用函数,并为不同类型的第一个参数注册不同的实现。它是 Python 中对函数进行多态操作的一种方式,类似于其他编程语言中的方法重载(method overloading)。下面是一个简单的示例,展示如何使用 singledispatch
:
from functools import singledispatch
@singledispatch
def process(data):
raise NotImplementedError('Cannot process this type')
@process.register(int)
def _(data):
return f'Processing integer: {data}'
@process.register(str)
def _(data):
return f'Processing string: {data}'
@process.register(list)
def _(data):
return f'Processing list: {data}'
print(process(10)) # Processing integer: 10
print(process('Jack ')) # Processing string: Jack
print(process(['Jack ', 111, 888])) # Processing list: ['Jack ', 111, 888]
print(process({'name': 'Jack '})) # NotImplementedError: Cannot process this type
在这个例子中:
-
定义通用函数
process
:使用@singledispatch
装饰器将process
定义为一个单分派泛型函数。这个函数在没有匹配类型的实现时,会抛出NotImplementedError
。 -
注册具体类型的实现:使用
@process.register(type)
为不同的类型注册具体的实现。例如,int
类型、str
类型和list
类型。 -
原理与实现:
-
singledispatch
基于被装饰的通用函数,创建一个调度机制,该机制会根据第一个参数的类型调用相应的注册实现。具体来说: -
@singledispatch
装饰器将一个函数标记为通用函数。 -
使用
@通用函数.register(type)
语法,为特定类型注册具体的实现。 -
调用通用函数时,根据第一个参数的类型,调度器会找到并调用相应的实现。
functools.singledispatch
在以下场景中非常有用:
-
数据处理:处理不同类型的数据,例如字符串、数字、列表、字典等,可以根据数据类型调用相应的处理逻辑。
-
API 设计:在设计 API 时,根据传入参数的类型调用不同的处理函数,使 API 更加灵活和可扩展。
-
类型特定的操作:在科学计算、数据分析等领域,根据输入数据类型执行特定操作,例如矩阵运算、数据转换等。
使用singledispatchmethod
装饰类方法:
Python 3.8 引入了 singledispatchmethod
,它允许你在类中使用单分派泛型方法。下面是一个示例:
from functools import singledispatchmethod
class DataHandler:
@singledispatchmethod
def handle(self, data):
raise NotImplementedError(f'Cannot handle type {type(data)}')
@handle.register(int)
def _(self, data):
return f'Handling integer: {data}'
@handle.register(str)
def _(self, data):
return f'Handling string: {data}'
handler = DataHandler()
print(handler.handle(10))
print(handler.handle('Jack '))
try:
print(handler.handle(['Jack', 999]))
except NotImplementedError as e:
print(e)
结果:
Handling integer: 10
Handling string: Jack
Cannot handle type <class 'list'>
functools.singledispatch
是一个强大的工具,用于根据第一个参数的类型选择不同的函数实现,简化了多态函数的编写。它适用于需要根据输入参数类型执行不同逻辑的场景,提高了代码的可读性和维护性。在 Python 3.8 及以上版本中,可以使用 singledispatchmethod
装饰类方法,实现类似的多态行为。
八、functools.cached_property ( >= Python 3.8)
functools.cached_property
是 Python 3.8 引入的一个装饰器,用于将类方法转换为属性,并缓存其结果。与普通属性不同,cached_property
只在第一次访问时计算一次,并将结果缓存起来,后续访问将直接返回缓存的结果,而不会重新计算。这对于一些计算开销较大的属性非常有用,可以显著提高性能。
functools.cached_property
的作用:
cached_property
的主要作用是将类的方法转换为只计算一次并缓存结果的属性。这可以减少重复计算,提高访问速度。下面是一个简单的示例,展示如何使用 cached_property
:
from functools import cached_property
class Data:
def __init__(self, value):
self.value = value
@cached_property
def expensive_computation(self):
print('Performing expensive computation...')
return self.value * 2
data = Data(10)
print(data.expensive_computation)
print(data.expensive_computation) #第二次直接调用缓存结果
结果:
Performing expensive computation...
20
20
在这个例子中,expensive_computation
方法被 @cached_property
装饰,第一次访问时会进行计算并打印“Performing expensive computation...”,之后的访问将直接返回缓存的结果。
应用场景:
-
昂贵的计算:当计算某个属性的值非常耗时,并且在对象的生命周期中多次访问时,使用
cached_property
可以显著提高性能。 -
不可变对象:对于不可变对象,属性值一旦计算出来就不会改变,因此缓存这些值非常有用。
3- 延迟计算:某些属性的计算可能依赖于其他属性,使用它可以实现延迟计算,直到属性第一次被访问时才进行计算。
注意事项:
-
不可变性:
cached_property
适用于那些在实例生命周期内不会改变的属性。如果属性可能会改变,则需要显式清除缓存或使用其他机制。 -
线程安全:在多线程环境中,
cached_property
可能需要额外的同步措施,以确保属性的计算和缓存是线程安全的。
functools.cached_property
是一个非常有用的工具,用于将类的方法转换为只计算一次并缓存结果的属性。它可以显著提高属性计算开销较大的类的性能,尤其适用于不可变对象和需要延迟计算的场景。通过缓存计算结果,cached_property
可以减少重复计算,提升程序效率。
九、参考资料
1、 Python高级编程:Functools模块的8个高级用法