前言
之前的文章中提到了 functools模块中的reduce 和 wraps, functools模块提供了处理函数的高阶函数, 再不重写原函数的情况下, 使用或者拓展原函数的功能. 本文介绍一下functools中的其他常用模块. 熟练运用可以使代码更加高性能, 易读易维护的代码.下面开始吧
1. 偏函数 partial 和 partialmethod
partial作为一个函数式编程中的高阶函数,其功能就是为某个已经存在的函数对象提供了一种简洁的绑定函数参数的方式。对于关键字参数,相当于提供默认值,对位置参数相当于冻结参数。
重要的是,这种参数绑定,不是在函数定义阶段(比如默认值参数),而是借助外部工具partial来进行参数的绑定,返回一个参数缩减的特化版本,这个绑定过程可以发生在编写代码的阶段(简单的得到一个特化函数),甚至可以发生在运行时,通过配置文件或者交互输入解析待绑定的参数,来动态的生成特定功能的函数。
使用方法
partial(func, *args, **kwargs)
对函数func通过args和kwargs进行参数绑定, 生成一个新函数. 下面通过一个实例来说明一下
In [1]: from functools import partial
In [2]: def calculate(operator, x, y):
...: ret = eval(f'{x} {operator} {y}')
...: print(ret)
In [3]: calculate('+', 4, 2)
4 + 2 = 6
定义一个calculate函数, 传入运算符和两个数据, 返回计算的结果. 内置函数描述eval() 函数用来执行一个字符串表达式.这里不详细介绍. 为了使用方便, 想显式的定义一个加减乘除方法, 需要重新定义 add ,sub, multiply, divide的方法.
In [4]: def add(x, y):
...: return calculate('+', x, y)
In [5]: def sub(x, y):
...: return calculate('-', x, y)
In [6]: def mul(x, y):
...: return calculate('*', x, y)
In [7]: def div(x, y):
...: return calculate('/', x, y)
如果函数的参数非常多, 需要写很多重复代码, 一旦参数发生变化, 需要维护多个函数也比较麻烦. 可以partial达到同样的效果, 代码的可维护性提高很多
In [8]: add = partial(calculate, '+')
In [9]: add(5, 9)
5 + 9 = 14
In [10]: sub = partial(calculate, '-')
In [11]: sub(10,3)
10 - 3 = 7
In [12]: sub5 = partial(calculate, '-', y=5)
In [13]: sub5(9)
9 - 5 = 4
通过partial将calculate函数的参数operator冻结成'+', 生成新的方法add. 可以"冻结"多个参数operator='-', y=5来生成新的方法sub5, 返回一个数据减5之后的结果. 这样的代码更加的灵活.partial创建的对象提供3个只读属性
- partial.func - 它返回父函数的名称以及十六进制地址。
- partial.args - 它返回部分函数中提供的位置参数。
- partial.keywords - 它返回部分函数中提供的关键字参数。
In [14]: sub5.func
Out[14]: <function __main__.calculate(operator, x, y)>
In [15]: sub5.args
Out[15]: ('-',)
In [16]: sub5.keywords
Out[16]: {'y': 5}
1.1 partialmethod
patialmethod和partial 的使用方法相同, 只是输入和返回都是一个类的方法
In [1]: from functools import partialmethod
In [2]: class Request:
...: def send(url, method, query, body, header, **kwargs):
...: ...
...:
...: get = partialmethod(send, method='GET')
...: post = partialmethod(send, method='POST')
...: put = partialmethod(send, method='PUT')
2. singledispatch
因为python是动态类型的语言, 没有编译过程中的类型检查, 所以可以支持这种代码出现.
In [1]: def ad(x, y):
...: return x + y
In [2]: ad(2, 5)
Out[2]: 7
In [3]: ad('aaa', 'bbb')
Out[3]: 'aaabbb'
In [4]: ad(['A', 'B'], ['MM', 'GG'])
Out[4]: ['A', 'B', 'MM', 'GG']
这是python的一大特性, 提供了非常好的灵活性. 但是同时也带来了一些不足,
① 是可读性变差, 如果没有非常规范的注释,并不能快速知道参数都是做什么的, 这就是为什么Python3.6 引入了类型注解 def ad(x: int, y: int) -> int: 这种写法 .
② 如果函数内需要根据数据类型进行不同的处理, 需要写if 语句进行类型判断. 类似如下代码
In [1]: def lower(data):
...: if isinstance(data, (int, float)):
...: return data
...: elif isinstance(data, str):
...: return data.lower()
...: elif isinstance(data, list):
...: return [lower(d) for d in data]
In [2]: lower('SME')
Out[2]: 'sme'
In [3]: lower([2, 'AMV', ['sports', 'OP']])
Out[3]: [2, 'amv', ['sports', 'op']]
函数lower将输入的字符转换成小写, 如果输入是数字返回原数据, 如果输入是列表, 则遍历列表内每一个元素, 将其执行lower函数 转换其中包含的大写字符.
可以看到lower函数中要写很多行的if语句, 如果是一个处理流程比较复杂的函数的话, 整个函数代码会变成含有 多层缩进和嵌套的冗长代码, 难以阅读和维护.
def func(data):
if isinstance(data, (int, float)):
for x in xxx:
...
if condition:
...
return data
elif isinstance(data, str):
if condition:
...
if condition:
...
elif isinstance(data, list):
...
这种场景下就可以使用 singledispatch.
singledispatch是一个函数装饰器。它将函数转换为泛型函数,以便根据其第一个参数的类型具有不同的行为。它用于函数重载,重载是依赖于 register() 注册属性来实现的.
In [1]: from functools import singledispatch
In [2]: @singledispatch
...: def lower(data):
...: return data
In [3]: @lower.register(str)
...: def _(data):
...: return data.lower()
In [4]: @lower.register(list)
...: def _(data):
...: return [lower(d) for d in data]
In [5]: lower([2, 'AMV', ['sports', 'OP']])
Out[5]: [2, 'amv', ['sports', 'op']]
singledispatch可以理解为将多个函数, 逻辑上认为是1个函数, 然后根据第一个参数的类型, 由dispatcher来判断,指定对应的函数来执行响应并返回.
singledispatchmethod 和 singledispatch 用法相同, 只是被装饰对象为类的方法.
3. 最近使用缓存LRU_cache (Least Recently Used)
LRU_cache 是一个函数装饰器,用于保存最多 maxsize 个函数的最近调用。这可以在使用相同参数重复调用的情况下节省时间和内存。
通俗点讲就是使用一块缓存存储被装饰函数的参数和返回, 后续遇到同样的参数, 直接从缓存中查询结果返回, 而不是再次计算.
以一个通过递归实现Fibonacci数列函数为例子. 没有缓存的时候, 如果我们要求fibo(5)的值,
可以看到 fibo(4) 运行了1次, fibo(3)运行了2次, fibo(2) 运行了3次, fibo(1)运行了2次, 随着要求的值的增大, 运行的函数次数也成指数增长.
In [1]: from timeit import timeit
In [2]: def fibo(n):
...: if n in (0, 1):
...: return 1
...: else:
...: return fibo(n-1) + fibo(n-2)
In [3]: timeit(lambda: fibo(40), number=1)
Out[3]: 43.062452300000004
通过以上代码可以看到, 当求fibo(40)的时候, 用时已经达到了43秒.
如果使用了缓存, fibo(3)第二次执行时, 就不需再次计算, 将函数执行的次数大幅减少
如 fibo(40) 之前要执行 165580141 次函数, 使用了缓存只需要执行 40次, 其余的数据可以通过查询获取. 看一下加入了lru_cache的性能表现
In [1]: from timeit import timeit
In [1]: from functools import lru_cache
In [2]: @lru_cache(50)
...: def fibo(n):
...: if n in (0, 1):
...: return 1
...: else:
...: return fibo(n-1) + fibo(n-2)
In [3]: timeit(lambda: fibo(40), number=1)
Out[3]: 2.9800000000079763e-05
性能提升了1445048倍.
总结
本文介绍了functools的 3个常用模块
- partial , 通过"冻结"现有函数的参数, 生成新的函数
- singledispatch, 根据第一个参数类型来分发到不同的函数进行执行, 使代码可读性更好, 适用于依参数数据类型不同进行不同处理的函数
- lru_cache, 将函数最近的调用进行缓存, 遇到相同参数调用, 直接查询缓存返回, 适用于递归调用的函数.
- wraps, 保留被装饰函数的信息, 配合装饰器使用, 相关文章
- reduce, 将可迭代对象减少为单个值, 相关文章
functools 模块还有其他的方法, cmp_to_key, Total_ordering等, 本文暂不介绍