这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战
Python标准库包括许多强大的模块,可以让你的代码既干净又高效,今天我们介绍的是functools
模块,这个模块提供了许多有用的高阶函数,我们可以利用这些高阶函数来实现缓存功能、重载、创建装饰器等,会然我们的代码更加高效,好了,让我们来看一下,functools
模块都给我们提供了什么呢。
缓存
functools
模块中提供了一个简单却又强大的缓存函数(装饰器)lru_cache()
,lru_cache()
可以直接以语法糖@
的形式作用于我们的函数至上,为我们的函数提供缓存的功能,遵循最近最少使用的原则,将函数执行结果缓存,方便下次函数执行前查询,如果函数的参数比较固定,逻辑比较固定,那么将非常必要,可以有效的减少函数的执行时间。
import functools
import time
@functools.lru_cache(maxsize=32)
def add(a, b):
print("sleep 2s")
time.sleep(2)
return a+b
print("第1次调用:", add(4, 5)) # 第一次调用,无缓存,执行原函数,结果加入缓存
print("第2次调用:", add(4, 5)) # 第二次调用,参数与第一次相同,匹配到缓存,直接返回缓存,不执行原函数
print("第3次调用:", add(5, 5)) # 第三次调用,参数与之前不同,无缓存,执行原函数,结果加入缓存
print(add.cache_info()) # 检查函数的缓存信息,它显示缓存命中和未命中的数量
复制代码
执行结果为:
sleep 2s
第1次调用: 9
第2次调用: 9
sleep 2s
第3次调用: 10
CacheInfo(hits=1, misses=2, maxsize=32, currsize=2)
Process finished with exit code 0
复制代码
在这个例子中,我们使用@lru_cache
装饰器处理add()
函数并缓存执行的结果(可以缓存32个结果)。为了查看缓存是否真的有效,我们使用了cache_info()
方法检查函数的缓存信息,它显示缓存命中和未命中的数量,通过结果我们可以看出,在第一次调用的时候并无缓存,add()
函数执行结果存入缓存,第二次的时候命中缓存,第三次的时候没有命中缓存。
如果你想要更细粒度的缓存,那么你也可以包含可选的typed=true
参数,这使得不同类型的参数被分别缓存。
例如,add(4.0, 5.0)
和add(4, 5)
将被视为不同的调用。
其实在Python3.8的版本之后,还有另一个可以用于缓存的装饰器叫做cached_property
,这个函数用于缓存类属性的结果,如果你的类的属性是而又不可变的,可以使用这个。
比较
您可能已经知道,可以使用__lt__()
、__gt__()
和__eq__()
在Python中实现比较操作符,如<
、>
或==
。但是实现__lt__()
、__gt__()
、__eq__()
、__le__()
、__ge__()
可能是相当繁琐的,刚好functools
模块包含total_ordering
‘装饰器,它可以帮助我们做到这一点。
我们只需要实现__eq__()
,和__lt__()
、__gt__()
、__le__()
、__ge__()
中的任一个方法,其他的它会帮我们自动提供:
from functools import total_ordering
@total_ordering
class MyNumber:
def __init__(self, value):
self.value = value
def __gt__(self, other):
return self.value > other.value
def __eq__(self, other):
return self.value == other.value
print(MyNumber(5) > MyNumber(3))
# True
print(MyNumber(1) < MyNumber(5))
# True
print(MyNumber(5) >= MyNumber(5))
# True
print(MyNumber(5) <= MyNumber(2))
# False
复制代码
很明显,它可以帮助我们减少代码和提高可读性。
partial函数(偏函数)
这里的偏函数和数学意义上的偏函数不是一个概念,functools
模块中的partial
函数的作用是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会比调用原来的函数更简单。理解起来是不是有些困惑,让我们来看一些实际的例子。
在Python中,int()
函数可以把指定数字字符串转换为整数,默认是做十进制的转换:
print(int(1234))
# 1234
复制代码
但int()
函数还提供了一个base
参数,默认值为10
。如果传入base
参数,就可以做N进制的转换,即,默认数字字符串的进制是N进制,要转化为10进制,例如:
print(int("1234", base=8))
# 默认1234位8进制的数字,转化为10进制:668
print(int("1234", base=16))
# 默认1234位16进制的数字,转化为10进制:4660
复制代码
假设要转换大量的8进制字符串,每次都传入int(x, base=8)
显得过于麻烦,于是,我们想到,可以定义一个int8()
的函数,默认把base=8
传进去:
def int8(x, base=8):
return int(x, base)
print(int8("1234"))
复制代码
这样,就非常方便了。
其实functools.partial
的功能就是能够创建一个偏函数,不需要我们自己定义int8()
,可以直接使用下面的代码创建一个新的函数int8
:
int2 = functools.partial(int, base=8)
print(int2('1234'))
复制代码
所以简单来说,functools.partial
的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,使得我们调用这个新函数会更简单。
重载
我们都知道严格意义上来说,在Python中函数重载是不可能实现的,但实际上有一个简单的方法来实现和函数重载相似的功能,那就是使用functools
模块中singledispatch
函数装饰器。下面来看个例子:
@singledispatch
def show(obj):
print(obj, type(obj), "obj")
@show.register(str)
def _(text):
print(text, type(text), "str")
@show.register(int)
def _(n):
print(n, type(n), "int")
show(1234)
show("abcd")
show({"abcd": 1234})
复制代码
结果为:
1234 <class 'int'> int
abcd <class 'str'> str
{'abcd': 1234} <class 'dict'> obj
Process finished with exit code 0
复制代码
可见为show()
函数传递不同的类型参数,就调用不同的函数,表现不同的行为,实际上singledispatch
实现的是单泛函数,给函数加上.register
方法,该方法支持绑定一个变量类型和一个函数。然后它返回一个被重载了的函数。当输入值的类型是通过.register
绑定的类型时,就调用同时绑定的函数。
wraps
这一部分在我之前的文章有提到,感性趣的可以看一下:内置装饰器@functools.wrap
总结
functools
模块提供了许多有用的功能和装饰器,可以帮助我们构建更好的代码,我这里提到的只是部分内容。感兴趣的小伙伴可以查看官方文档functools — Higher-order functions and operations on callable objects
最后,感谢女朋友在工作和生活中的包容、理解与支持 !