functools模块
functools.partial(func, /, *args, **keywords): 有的翻译为偏函数,本质上是装饰器和闭包。 给目标函数传入指定的实参后,返回一个新的函数(其实是Partial对象,但仍然是callable),新函数会"冻结"一部分参数(用位置传参的参数,在调用新函数时无法再改变其实参,但用关键字传参的参数,在调用新函数时仍然可以通过关键字传参来改变实参)。
该函数的大致的等价实现如下:
def partial(func, /, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = {**keywords, **fkeywords}
return func(*args, *fargs, **newkeywords)
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc
下面示例演示了我们在调用partial函数时,用位置传参与用关键字传参,会导致生成的Partial对象有不同的签名。
import inspect
from functools import partial
def add(x,y):
print(x,y)
return x + y
new_fun = partial(add, y=5)
print(new_fun(3))
print(inspect.signature(new_fun))
print('*' * 50)
new_fun2 = partial(add, x=3)
print(new_fun2(y=4))
print(inspect.signature(new_fun2))
print('*' * 50)
new_fun3 = partial(add, 3)
print(new_fun3(4))
print(inspect.signature(new_fun3))
输出结果:
3 5
8
(x, *, y=5)
**************************************************
3 4
7
(*, x=3, y)
**************************************************
3 4
7
(y)
Patial对象可以当做函数再次传给partial函数。
import inspect
from functools import partial
def add(x,y,z):
print(x, y, z)
print(inspect.signature(add))
print('*' * 50)
new_fun = partial(add, 3)
print(inspect.signature(new_fun))
print('*' * 50)
new_fun2 = partial(new_fun, 4)
print(inspect.signature(new_fun2))
输出结果:
(x, y, z)
**************************************************
(y, z)
**************************************************
(z)
下面演示,当partial函数使用关键字传参来生成Partial对象时,Partial签名里相当于有了关键字参数及缺省值,因此我们可以通过对Partial对象进行关键字传参,用实参来替代缺省值。
def add(x,y,z):
print(x, y, z)
print(inspect.signature(add))
print('*' * 50)
new_fun = partial(add, x=3)
print(inspect.signature(new_fun))
print('*' * 50)
new_fun2 = partial(new_fun, y=4)
print(inspect.signature(new_fun2))
print('*' * 50)
new_fun2(y=5,z=6) #通过关键字传参,来修改Partial对象签名中的关键字参数
输出结果:
(x, y, z)
**************************************************
(*, x=3, y, z)
**************************************************
(*, x=3, y=4, z)
**************************************************
3 5 6
functools.partialmethod(func, /, *args, **keywords): 功能及用法都与partial函数类似,只不过它应该用在类内部,作用于类中的方法上。
from functools import partialmethod
class MyClass:
def __init__(self, value):
self.value = value
def multiply(self, x, y):
print(x, y)
return self.value * x * y
# 使用 partialmethod 创建一个新的方法,预先绑定 y=2
multiply_by_2 = partialmethod(multiply, y=2)
# 创建实例
obj = MyClass(10)
# 调用原方法
print(obj.multiply(3, 4)) # 输出: 120 (10 * 3 * 4)
# 调用部分应用的方法
print(obj.multiply_by_2(3)) # 输出: 60 (10 * 3 * 2)
functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES): 此函数解决了装饰器的一个问题,即装饰一个函数后,返回的新函数的__doc__和__name__等属性被替换成了wrapper的属性,而非原函数的属性。
下面示例展示普通装饰器的效果:
def decorator(fn):
def wrapper(*args, **kwargs):
'''这是wrapper函数'''
print('打印参数', args, kwargs)
return fn(*args, **kwargs)
return wrapper
@decorator
def add(x, y):
'''这是add函数'''
return x + y
print(add(3,4))
print(add.__name__)
print(add.__qualname__)
print(add.__doc__)
输出结果:
打印参数 (3, 4) {}
7
wrapper
decorator.<locals>.wrapper
这是wrapper函数
现在我们看下update_wrapper函数的使用。
from functools import update_wrapper
def decorator(fn):
def wrapper(*args, **kwargs):
'''这是wrapper函数'''
print('打印参数', args, kwargs)
return fn(*args, **kwargs)
update_wrapper(wrapper, wrapped=fn)
return wrapper
@decorator
def add(x, y):
'''这是add函数'''
return x + y
print(add(3,4))
print(add.__name__)
print(add.__qualname__)
print(add.__doc__)
输出结果:
打印参数 (3, 4) {}
7
add
add
这是add函数
@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES): 此函数是update_wrapper的装饰器用法,功能一样。
下面示例用wraps装饰器替换上面的update_wrapper方法。注意,不论是使用装饰器,还是update_wrapper,返回的新函数都多了一个__wrapped__属性,指向原函数。
from functools import wraps
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
'''这是wrapper函数'''
print('打印参数', args, kwargs)
return fn(*args, **kwargs)
return wrapper
@decorator
def add(x, y):
'''这是add函数'''
return x + y
print(add(3,4))
print(add.__name__)
print(add.__qualname__)
print(add.__doc__)
print(add.__wrapped__)
输出结果:
打印参数 (3, 4) {}
7
add
add
这是add函数
<function add at 0x0000014D1841A5C0>
@functools.singledispatch: 单派发泛型函数装饰器。 就是为python实现对函数的重载。 以第一个形参的类型做为判断条件,不同类型为函数重载。
对一个函数使用此装饰器后,以被装饰的函数.register为装饰器去装饰另一个函数。 如果被装饰的函数的第一个形参有类型提示,则register不需要传参数,否则,需要传递类型参数。
下面示例因为被装饰的函数fun_str(x, y)第一个参数没有类型提示,而register也没传递类型参数,所以会报错。
from functools import singledispatch
@singledispatch
def fun(x, y):
print("参数是object", type(x), x, type(y), y)
@fun.register
def fun_str(x, y):
print("参数是str", type(x), x, type(y), y)
输出结果:
Traceback (most recent call last):
File "C:\WorkFolder\PythonProjects\pythonStudy\fortest.py", line 7, in <module>
@fun.register
^^^^^^^^^^^^
File "C:\WorkFolder\FreeInstallSoftware\Python\Python311\Lib\functools.py", line 870, in register
raise TypeError(
TypeError: Invalid first argument to `register()`: <function fun_str at 0x00000274ED1E0B80>. Use either `@register(some_class)` or plain `@register` on an annotated function.
对其修改, 将被装饰的参数的第一个形参添加类型提示后,运行通过。
from functools import singledispatch
@singledispatch
def fun(x, y):
print("参数是object", type(x), x, type(y), y)
@fun.register
def fun_str(x: str, y):
print("参数是str", type(x), x, type(y), y)
fun(123, 456)
fun('123', 456)
输出结果:
参数是object <class 'int'> 123 <class 'int'> 456
参数是str <class 'str'> 123 <class 'int'> 456
下面是另一种方式,不加类型提示,但是register传递类型参数。
from functools import singledispatch
@singledispatch
def fun(x, y):
print("参数是object", type(x), x, type(y), y)
@fun.register(str)
def fun_str(x, y):
print("参数是str", type(x), x, type(y), y)
fun(123, 456)
fun('123', 456)
输出结果:
参数是object <class 'int'> 123 <class 'int'> 456
参数是str <class 'str'> 123 <class 'int'> 456
可以看到,输出效果相同。
由于添加重载函数时,函数名已经不重要了,所以可以用_来代替。 另外,当调用函数时的实参类型无法匹配到用register函数装饰的重载函数时,就会调用默认的函数,即使用@singledispatch装饰的函数, 上面的示例中实参传递是整数的情况就体现了这一点。
from functools import singledispatch
@singledispatch
def fun(x, y):
print("参数是object", type(x), x, type(y), y)
@fun.register(str)
def fun_str(x, y):
print("参数是str", type(x), x, type(y), y)
@fun.register
def _(x: int, y):
print("参数是int", type(x), x, type(y), y)
fun(123, 456)
fun('123', 456)
输出结果:
参数是int <class 'int'> 123 <class 'int'> 456
参数是str <class 'str'> 123 <class 'int'> 456
形参的类型提示可以合并多个类型。
from functools import singledispatch
@singledispatch
def fun(x, y):
print("参数是object", type(x), x, type(y), y)
@fun.register
def _(x: float|None, y):
print("参数是float或None", type(x), x, type(y), y)
fun(123.0, 456)
fun(None, 456)
输出结果:
参数是float或None <class 'float'> 123.0 <class 'int'> 456
参数是float或None <class 'NoneType'> None <class 'int'> 456
我们还可以用被@singledispatch装饰的函数名.dispatch,然后传递一个类型,返回一个对应的函数对象。
from functools import singledispatch
@singledispatch
def fun(x, y):
print("参数是object", type(x), x, type(y), y)
@fun.register
def _(x: int, y):
print("参数是int", type(x), x, type(y), y)
@fun.register
def _(x: float|None, y):
print("参数是float或None", type(x), x, type(y), y)
print(fun.dispatch(int)) # <function _ at 0x00000209970A0B80>
print(fun.dispatch(float)) # <function _ at 0x00000209970A0C20>
print(fun.dispatch(type(None))) # <function _ at 0x00000209970A0C20>
fun.dispatch(type(None))(999, 888) # 参数是float或None <class 'int'> 999 <class 'int'> 888
输出结果:
<function _ at 0x00000191A2BD0B80>
<function _ at 0x00000191A2BD0C20>
<function _ at 0x00000191A2BD0C20>
参数是float或None <class 'int'> 999 <class 'int'> 888
functools.singledispatchmethod(func): 用法与functools.singledispatch一样,只不过它是用来重载类定义中的方法。
from functools import singledispatchmethod
class Foo:
@singledispatchmethod
def fun(self, x, y):
print("参数是object", type(x), x, type(y), y)
@fun.register
def _(self, x: int, y):
print("参数是int", type(x), x, type(y), y)
@fun.register
def _(self, x: float | None, y):
print("参数是float或None", type(x), x, type(y), y)
foo = Foo()
foo.fun(123, 456)
foo.fun(None, 456)
输出结果:
参数是int <class 'int'> 123 <class 'int'> 456
参数是float或None <class 'NoneType'> None <class 'int'> 456
@functools.lru_cache: 缓存函数返回值的装饰器。支持多线程使用。 有两种方式,一种是不带参数的,一种是带参数的。 不带参数的等价于传给参数缺省值 @functools.lru_cache(maxsize=128, typed=False) 。
被装饰的函数,调用时,其实参及对应的返回值被缓存起来。 缓存(LRU), least recently used, 最近最少使用缓存,即最近一段时间最少被使用的数据会被新数据挤出缓存。 函数调用时传递参数方式不同,不会当成相同的key来缓存。 缓存的key就是函数的实参,值就是函数返回值。 命中缓存时,函数直接返回值,而不会执行函数内部其它代码。
maxsize是缓存的数量,即key的数量。 typed=False,代表不同参数类型视为不同的key,比如foo(x=3)和foo(x=3.0)。
可以调用cache_info()函数来查看缓存信息, 调用cache_clear()函数清空缓存。最后还有一个cache_parameters()函数用来查看当时装饰时传递的参数,这个功能看起来没啥用。
由于缓存是以mapping表结构存储的,key是实参,所以实参必须是可hash的。 比如,实参如果是列表,就无法缓存。
from functools import lru_cache
import time
@lru_cache(maxsize=3) # maxsize 指定缓存的最大条目数
def my_function(x):
print(f"Computing for {x}")
time.sleep(1)
return x * x
print(my_function(2)) # 计算并缓存
print(my_function(3)) # 计算并缓存
print(my_function(4)) # 计算并缓存
print('-' * 25, '分隔线', '-' * 25)
print(my_function(5)) # 计算并缓存
print('-' * 25, '分隔线', '-' * 25)
print(my_function(3)) # 直接从缓存中取出
print(my_function(4)) # 直接从缓存中取出
print(my_function(2)) # 2被移出缓存了,需重新计算
print('-' * 25, '分隔线', '-' * 25)
print(my_function.cache_info())
print('-' * 25, '分隔线', '-' * 25)
my_function.cache_clear() # 清空缓存,之后会重新统计缓存命中率
print(my_function.cache_info())
print(my_function(2))
print(my_function.cache_info())
print('-' * 25, '分隔线', '-' * 25)
print(my_function(2)) # 命中缓存
print(my_function.cache_info())
print('-' * 25, '分隔线', '-' * 25)
print(my_function(x=2)) # 调用函数时传参方式不一样不能命中缓存。
print(my_function.cache_info())
print('-' * 25, '分隔线', '-' * 25)
print(my_function.cache_parameters()) # 此函数用于查看装饰时使用的参数
输出结果:
Computing for 2
4
Computing for 3
9
Computing for 4
16
------------------------- 分隔线 -------------------------
Computing for 5
25
------------------------- 分隔线 -------------------------
9
16
Computing for 2
4
------------------------- 分隔线 -------------------------
CacheInfo(hits=2, misses=5, maxsize=3, currsize=3)
------------------------- 分隔线 -------------------------
CacheInfo(hits=0, misses=0, maxsize=3, currsize=0)
Computing for 2
4
CacheInfo(hits=0, misses=1, maxsize=3, currsize=1)
------------------------- 分隔线 -------------------------
4
CacheInfo(hits=1, misses=1, maxsize=3, currsize=1)
------------------------- 分隔线 -------------------------
Computing for 2
4
CacheInfo(hits=1, misses=2, maxsize=3, currsize=2)
------------------------- 分隔线 -------------------------
{'maxsize': 3, 'typed': False}
@functools.cache(user_function): 功能与lru_catche有类似,不需要传参数,不限定缓存大小,因为不需要清除旧值,所以比带有大小限制的 lru_cache() 更小更快。支持多线程使用。
@functools.cached_property(func): 类似于@property装饰器,但增加了缓存功能。 将类中的方法转变成属性,并缓存其值。 而且后续还可以直接修改属性的值。生命周期同实例对象相同。
在方法(即被装饰后的属性)第一次调用时计算其值,并进行缓存。 下面示例中通过vars(obj)方法展示了实例的__dict__的前后变化。
from functools import cached_property
import time
class MyClass:
@cached_property
def expensive_computation(self):
print("Calculating...")
time.sleep(1)
return 42 * 42 # 假设这里是一个计算量很大的操作
obj = MyClass()
print(vars(obj))
print('-' * 25, '分隔线', '-' * 25)
print(obj.expensive_computation) # 第一次访问,计算并缓存
print(obj.expensive_computation) # 第二次访问,直接返回缓存结果
print(vars(obj))
print('-' * 25, '分隔线', '-' * 25)
obj.expensive_computation = 100 # 缓存的属性可以直接修改其值
print(obj.expensive_computation) # 第一次访问,计算并缓存
print(vars(obj))
输出结果:
{}
------------------------- 分隔线 -------------------------
Calculating...
1764
1764
{'expensive_computation': 1764}
------------------------- 分隔线 -------------------------
100
{'expensive_computation': 100}
@functools.total_ordering: 该装饰器用来装饰类,帮助我们自动补全比较方法。 我们只提供__eq__和另一个方法(比如__lt__),该装饰器就会帮我们自动推导出其他比较方法的实现(例如!=,<=,>,>=)。
但该装饰器补充的比较方法的性能稍慢,如果这会影响程序的性能,通常我们还是要手动补全其他比较方法的实现。
from functools import total_ordering
@total_ordering
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
# 定义相等操作符
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
# 定义小于操作符
def __lt__(self, other):
return (self.x, self.y) < (other.x, other.y)
# 使用 Point 类
p1 = Point(1, 2)
p2 = Point(2, 3)
p3 = Point(1, 2)
print(p1 < p2) # True
print(p1 == p3) # True
print(p2 > p1) # True
print(p2 >= p1) # True

浙公网安备 33010602011771号