functools是一个函数增强器,主要为高阶函数使用,作用于或者返回其他函数的函数,通常任何可调用的对象都可视为“函数”。主要包括以下几个函数:
cached_property
将类的方法转换为属性,该属性的值将被计算一次,然后在实例生命周期中作为常规属性进行缓存。与property()类似,但增加了缓存,对于计算复杂的属性很有用。cached_property在Python3.8之前的很多第三方库当中都有自己的实现,比如werkzeug.utils.cached_property、django.utils.functional.cached_property
举例如下:
# 在没有cached_property之前定义类属性
class DataSet:
def __init__(self):
self._data = None
@property
def data(self):
print('开始计算数据')
if not self._data:
# 计算data数据
self._data = 10 * 10
print('计算data数据')
return self._data
obj = DataSet()
print(obj.data)
# 输出
开始计算数据
计算data数据
100
print(obj.data)
# 输出
开始计算数据
100
使用变量记录属性数据,并在属性计算时进行判断,防止计算多次
from functools import cached_property
class DataSet:
@cached_property
def data(self):
print('开始计算数据')
return 10 * 10
obj = DataSet()
print(obj.data)
# 输出:
开始计算数据
100
print(obj.data)
# 输出:
100
可以看到,data属性函数只被计算了一次,而且无需额外定义变量计算。cached_property同时具有线程安全,在多线程中不会存在多次计算的问题。另外不支持python中的异步编程:asyncio。注意这个特性是在Python3.8中新增的。
cmp_to_key
将旧式比较功能转换为键功能。与接受关键功能的工具(例如sorted(),min(),max(),heapq.nlargest(),heapq.nsmallest(),itertools.groupby())一起使用。该函数主要用作从Python 2转换而来的程序的转换工具,该程序支持使用比较函数。
比较函数是任何可调用的函数,它们接受两个参数进行比较,小于返回一个负数,等于返回零,大于返回一个正数。键函数是一个可调用的函数,它接受一个参数并返回另一个值用作排序键。
from functools import cmp_to_key
l = [
{
'name': 'Tom',
'age': 12
},
{
'name': 'Join',
'age': 52
},
{
'name': 'Jeke',
'age': 23
}
]
def compare_func(a, b):
if a.get('age') > b.get('age'):
return 1 #必须返回正数,不能是True
else:
return -1 #必须返回负数,不能是False
print(sorted(l, key=cmp_to_key(compare_func)))
# 输出:
[{'name': 'Tom', 'age': 12}, {'name': 'Jeke', 'age': 23}, {'name': 'Join', 'age': 52}]
在python2中sorted的函数原型是:sorted(iterable, cmp=None, key=None, reverse=False),参数中包含一个cmp参数,来提供让我们传入一个自定义函数的参数,但是python3 中的sorted函数原型是:sorted(iterable, /, *, key=None, reverse=False),这里出现了/,*两个符号,上一篇我们介绍过,主要是后面没有了cmp参数,自定义函数排序就很不方便。这时候functools.cmp_to_key就为我们提供了这样一个自定义函数排序方式,将函数转换为键功能-key
lru_cache
缓存装饰器,根据参数缓存每次函数调用结果,对于相同参数的,无需重新函数计算,直接返回之前缓存的返回值
- 如果maxsize设置为None,则禁用LRU功能,并且缓存可以无限制增长;当maxsize是2的幂时,LRU功能执行得最好;
- 如果 typed设置为True, 则不同类型的函数参数将单独缓存。例如,f(3)和f(3.0)将被视为具有不同结果的不同调用;
- 缓存是有内存存储空间限制的;
def a(x):
print(x)
return x+1
print(a())
# 输出:
3
4
print(a())
# 输出:
3
4
不使用缓存记录,每次都重新执行函数计算
from functools import lru_cache
@lru_cache()
def a(x):
print(x)
return x+1
print(a(3))
# 输出
3
4
print(a(3))
# 输出
4
print(a(4))
# 输出
4
5
使用缓存记录后,第一次a(3)调用,计算了数据后会进行缓存,第二次a(3)调用,因为参数相同,所以直接返回缓存的数据,第三次a(4)调用,因为参数不同,需要重新计算
partial
偏函数,可以扩展函数功能,但是不等于装饰器,通常应用的场景是当我们要频繁调用某个函数时,其中某些参数是已知的固定值,通常我们可以调用这个函数多次,但这样看上去似乎代码有些冗余,而偏函数的出现就是为了很少的解决这一个问题。
举一个简单的例子:
def add(a, b, c, x=1, y=2, z=3):
return sum([a, b, c, x, y, z])
print(add(1, 2, 3, x=1, y=2, z=3))
#输出
12
如果我们频繁调用此函数,并且固定传入某些参数,比如b=20, x=100
from functools import partial
def add(a, b, c, x=1, y=2, z=3):
print(a, b, c, x, y, z)
return sum([a, b, c, x, y, z])
add_100 = partial(add, 20, x=100)
print(add_100(1, 2, y=2, z=3))
# 输出
20 1 2 100 2 3
128
在进行函数重新定义时,如果需要固定非关键字参数,那么默认定义的是第一个非关键字参数;如果需要固定关键字参数,直接指定关键字即可。
实际上偏函数的使用更多是在回调函数时使用,举例如下:
register_func = []
def call_back(n):
print('call_back: ', n)
def call_back1(n, m):
print('call_back1: ', n, m)
# 注册回调函数
register_func.append((call_back, 10))
register_func.append((call_back1, 100, 200))
# 执行回调函数
for item in register_func:
func = item[0]
args = item[1:]
func(*args)
# 输出
call_back: 10
call_back1: 100 200
上面我们在注册回调函数的时候,需要记录函数名和各个参数,非常不方便,如果使用偏函数进行修饰
from functools import partial
register_func = []
def call_back(n):
print('call_back: ', n)
def call_back1(n, m):
print('call_back1: ', n, m)
call_back_partial = partial(call_back, 10)
call_back_partial1 = partial(call_back1, 100, 200)
# 注册回调函数
register_func.append(call_back_partial)
register_func.append(call_back_partial1)
# 执行回调函数
for func in register_func:
func()
# 输出
call_back: 10
call_back1: 100 200
对比上面的方式,偏函数定义的优势在哪里呢?
- 注册回调函数时,我们是知道函数参数的,所以在此使用偏函数很简单、很方便
- 使用偏函数后,注册回调函数和调用回调函数那里都使用完全固定的写法,无论传入的是固定参数、非固定参数或者关键字参数
- 相对于上面一点,只需要在注册的时候使用偏函数重新生成一个回调函数
这在回调函数的使用中是非常频繁、方便,而且爽就一个字
reduce
函数原型如下:
def reduce(function, iterable, initializer=None):
it = iter(iterable)
if initializer is None:
value = next(it)
else:
value = initializer
for element in it:
value = function(value, element)
return value
可以看到实际执行是将迭代器iterable中每一个元素传入function函数进行累计计算,并将最终值返回。一个简单的使用示例:
a=[1,3,5]
b=reduce(lambda x,y:x+y,a)
print(b)
# 输出
9
将a列表传入匿名函数进行累加计算
singledispatch
python函数重载,直接举例来说明
def connect(address):
if isinstance(address, str):
ip, port = address.split(':')
elif isinstance(address, tuple):
ip, port = address
else:
print('地址格式不正确')
# 传入字符串
connect('123.45.32.18:8080')
# 传入元祖
connect(('123.45.32.18', 8080))
简单来说就是address可能是字符串,也可能是元组,那么我们就需要在函数内进行单独处理,如果这种类型很多呢?那就需要if...elif...elif...elif..esle...,写起来非常不美观,而且函数的可读性也会变差。
学过C++和Java的同学都知道函数重载,同样的函数名,同样的参数个数,不同的参数类型,实现多个函数,程序运行时将根据不同的参数类型自动调用对应的函数。python也提供了这样的重载方式
from functools import singledispatch
@singledispatch
def connect(address):
print(f'传入参数类型为:{type(address)}, 不是有效的类型')
@connect.register
def connect_str(address: str):
ip, port = address.split(':')
print(f'参数为字符串,IP是{ip}, 端口是{port}')
@connect.register
def connect_tuple(address: tuple):
ip, port = address
print(f'参数为元组,IP是{ip}, 端口是{port}')
connect('123.45.32.18:8080')
# 输出
参数为字符串,IP是123.45.32.18, 端口是8080
connect(('123.45.32.18', '8080'))
# 输出
参数为元组,IP是123.45.32.18, 端口是8080
先使用singledispatch装饰器修饰connect函数,然后使用connect.register装饰器注册不同参数类型的函数(函数名可以随意,甚至不写,使用_代替),在调用的时候就会默认按照参数类型调用对应的函数执行。
total_ordering
定义一个类,类中定义了一个或者多个比较排序方法,这个类装饰器将会补充其余的比较方法,减少了自己定义所有比较方法时的工作量;
被修饰的类必须至少定义 __lt__(), __le__(),__gt__(),__ge__()中的一个,同时,被修饰的类还应该提供 __eq__()方法。简单来说就是只需要重载部分运算符,装饰器就会自动帮我们实现其他的方法。
class Person:
# 定义相等的比较函数
def __eq__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) ==
(other.lastname.lower(), other.firstname.lower()))
# 定义小于的比较函数
def __lt__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) <
(other.lastname.lower(), other.firstname.lower()))
p1 = Person()
p2 = Person()
p1.lastname = "123"
p1.firstname = "000"
p2.lastname = "1231"
p2.firstname = "000"
print(p1 < p2)
print(p1 <= p2)
print(p1 == p2)
print(p1 > p2)
print(p1 >= p2)
# 输出
True
Traceback (most recent call last):
File "/Volumes/Code/Python工程代码/Python基础知识/特殊特性学习/test.py", line 31, in
print(p1 <= p2)
TypeError: '<=' not supported between instances of 'Person' and 'Person'
报错在p1 <= p2这一行,提醒我们在Person对象之间不支持<=符号,使用total_ordering装饰器修饰以后。
from functools import total_ordering
@total_ordering
class Person:
# 定义相等的比较函数
def __eq__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) ==
(other.lastname.lower(), other.firstname.lower()))
# 定义小于的比较函数
def __lt__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) <
(other.lastname.lower(), other.firstname.lower()))
p1 = Person()
p2 = Person()
p1.lastname = "123"
p1.firstname = "000"
p2.lastname = "1231"
p2.firstname = "000"
print(p1 < p2)
print(p1 <= p2)
print(p1 == p2)
print(p1 > p2)
print(p1 >= p2)
# 输出
True
True
False
False
False
只在类上面增加了total_ordering装饰器,就可以完美支持所有的比较运算符了
wraps
python中的装饰器是“接受函数为参数,以函数为返回值”。但是装饰器函数也会有一些负面影响。我们来看一下例子:
# 普通函数
def add(x, y):
return x + y
print(add.__name__)
# 输出
add
# 装饰器函数
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def add(x, y):
return x + y
print(add.__name__)
# 输出
wrapper
可以看到函数名发生了变化,变为装饰器函数中的wrapper,除了__name__属性外还有其他属性,定义在WRAPPER_ASSIGNMENTS和WRAPPER_UPDATES变量中,包括__module__、__name__、 __qualname__、__doc__、__annotations__、__dict__。在很多情况下,我们需要对函数进行针对性处理,必须获取函数的模块属性进行处理,这个时候,就必须消除这种负面影响。functools.wraps就为我们解决了这个问题。
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def add(x, y):
return x + y
print(add.__name__)
# 输出
add
即使使用了装饰器修饰,我们仍然能获取到原函数的属性
update_wrapper
update_wrapper 的作用与 wraps 类似,不过功能更加强大,换句话说,wraps 其实是 update_wrapper 的特殊化,实际上 wraps(wrapped) 的函数源码为:
def wraps(wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES):
return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
使用方式:
from functools import update_wrapper
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return update_wrapper(wrapper, func)
@decorator
def add(x, y):
return x + y
print(add.__name__)
# 输出
add
注意:wraps和update_wrapper是专为装饰器函数所设计,而且强烈建议在定义装饰器时进行修饰