Functools-Python中高阶函数的力量

这是我参与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

最后,感谢女朋友在工作和生活中的包容、理解与支持 !

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享