python functools 使用详解

文章目录

  • 1、functools.cmp_to_key(func)
  • 2、@functools.lru_cache(maxsize=128, typed=False)
  • 3、functools.partial(func, /, *args, **keywords)
  • 4、functools.partialmethod(func, *args, **keywords)
  • 5、functools.reduce(function, iterable[, initializer])
  • 6、@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
    • functools.update_wrapper(wrapper, wrapped, **kargs)
  • 7、 @functools.total_ordering
  • 8、@functools.singledispatch

1、functools.cmp_to_key(func)

此函数主要用作将Python 2 程序转换至新版的转换工具,以保持对比较函数的兼容。
因为新版 python,sorted(iterable, *, key=None, reverse=False) 函数中 key 调用的函数,只能传入一个参数。 而旧版的 key 调用函数时,是需要传入两个参数的。

import functools


a = ['a', 'bb', 'c','dddd','eee']

""" =========== 旧版的排序调用函数 =============
    需要传入两个参数(列表中的前后两个元素),
    sorted函数通过判断这两个参数“运算”后的正负值,来确定排序。
    当值为 负 时,x 就排在 y前面(即系统认为 x 比 y小),
    当值为 正 时,x 就排在 y后面(即系统认为 x 比 y大)"""
def old_cmp(x, y):
    return len(x) - len(y)

b = sorted(a, key=functools.cmp_to_key(old_cmp))    # 通过 cmp_to_key 转为新版
print(b)      # >>> ['a', 'c', 'bb', 'eee', 'dddd']
    

""" =========== 新版的排序调用函数 =============
    只需要传入一个参数就可以了,sorted会自动的去判断列表中前后元素通过key的返回值大小"""
def new_cmp(i):
    return len(i)

n = sorted(a, key=new_cmp)
print(n)    # >>> ['a', 'c', 'bb', 'eee', 'dddd']

2、@functools.lru_cache(maxsize=128, typed=False)

一个为函数提供缓存功能的装饰器,缓存maxsize 组传入参数,在下次以相同参数调用时直接返回上一次的结果。用以节约高开销或I/O 函数的调用时间。
由于使用了字典存储缓存,所以该函数的固定参数和关键字参数必须是可哈希的。

如果 maxsize 设置为 None ,LRU功能将被禁用且缓存数量无上限。 maxsize 设置为2的幂时可获得最佳性能。

如果 typed 设置为true,不同类型的函数参数将被分别缓存。例如, f(3) 和 f(3.0) 将被视为不同而分别缓存。

为了衡量缓存的有效性以便调整 maxsize 形参,被装饰的函数带有一cache_info() 函数。当调用 cache_info() 函数时,返回一个具名元组,包含命中次数 hits(有效读取缓存的次数),未命中次数 misses ,最大缓存数量 maxsize 和 当前缓存大小 currsize。在多线程环境中,命中数与未命中数是不完全准确的。

该装饰器也提供了一个用于清理/使缓存失效的函数 cache_clear() 。

原始的未经装饰的函数可以通过 wrapped 属性访问。它可以用于检查、绕过缓存,或使用不同的缓存再次装饰原始函数。

一般来说,LRU缓存只在当你想要重用之前计算的结果时使用。
因此,如果用它缓存具有副作用的函数,即在每次调用的函数它所传入的参数和之前没有任何关联的(一样的部分),这样缓存是没有效果的

比较 下面 计算斐波那契数列的例子,一个用了缓存功能,一个没用缓存功能

import functools

@functools.lru_cache(maxsize=128)
def fib(n):
    print(f'计算第{n}个值')
    if n < 2:
        return n
    return fib(n-1)   fib(n-2)

print(fib(5))
print(fib.cache_info())    # 上面调用fib(5)之后,再调用 cache_info() 查看信息。

用了缓存功能的运行结果是 每个参数都只调用了一次函数:

计算第5个值
计算第4个值
计算第3个值
计算第2个值
计算第1个值
计算第0个值
5
CacheInfo(hits=3, misses=6, maxsize=128, currsize=6)

下面不用缓存功能:

import functools

#@functools.lru_cache(maxsize=128)
def fib(n):
    print(f'计算第{n}个值')
    if n < 2:
        return n
    return fib(n-1)   fib(n-2)


print(fib(5))

运算结果重复调用了好多次函数:

计算第5个值
计算第4个值
计算第3个值
计算第2个值
计算第1个值
计算第0个值
计算第1个值
计算第2个值
计算第1个值
计算第0个值
计算第3个值
计算第2个值
计算第1个值
计算第0个值
计算第1个值
5

3、functools.partial(func, /, *args, **keywords)

该函数用于为func 函数的部分参数指定参数值,从而得到一个转换后的函数,程序以后调用转换后的函数时,就可以少传入那些己指定值的参数。

import functools

def add(x,y):
    print(x y)

par_add = functools.partial(add,y=10)

par_add(5)     # >>> 15

需要注意的是,如果绑定的是 x 参数,那么 par_add()就需要用关键字参数来传递了,如下:

import functools

def add(x,y):
    print(x y)


par_add = functools.partial(add,x=10)

par_add(y = 5)     # >>> 15
# par_add(5)      #  如果是这样传递参数,那就相当于给 x 传了两个参数,会报错。

4、functools.partialmethod(func, *args, **keywords)

类似 3、partial(),这里 partialmethod() 是给类中的方法加参数的。

import functools

class People:
    def get_name(self, name, age):
        print(name, age)

    set_name = functools.partialmethod(get_name, '韩梅梅',18)    # 会自动传递 self 给 get_name(self, name, age) 函数

    def set_age(self):    # 和这个效果一样。
        self.get_name('哈哈',12)

p = People()
p.get_name('小红',10)

p.set_name()          # >>> 韩梅梅 18

p.set_age()           # >>> 哈哈 12

5、functools.reduce(function, iterable[, initializer])

将两个参数的 function 从左至右积累地应用到 iterable 的条目,以便将该可迭代对象缩减为单一的值。
类似 itertools 中的 accumulate( p [,func] ) 函数。
accumulate( ) 返回一个迭代器。
reduce( ) 返回一个值。

import functools

A = [1,2,3,4,5]

def add(x,y):
    return x y

S = functools.reduce(add, A)
print(S)      # >>> 15

S = functools.reduce(add, A, 100)    # 定义一个初始值 100
print(S)      # >>> 115

python functools 使用详解_第1张图片

6、@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

@functools.wraps() 主要就是让包装函数的 name 、doc 属性与
被包装函数保持一致。 函数的这些参数的默认值是模块级常量 WRAPPER_ASSIGNMENTS (它将被赋值给 wrapper 函数的 module, name, qualname, annotationsdoc 即文档字符串) 以及 WRAPPER_UPDATES (它将更新 wrapper 函数的 dict 即实例字典)。

1、先看没被 @functools.wraps()装饰的函数

import functools

def decorator(fun):
    def wrapper_func(*args):
        res = fun(*args)
        print(f'{fun.__name__} 函数的计算结果为:{res}')    
    return wrapper_func

@decorator
def add(x,y):
    return x y

add(4,5)
print(add.__name__)

运行结果:

add 函数的计算结果为:9
wrapper_func

可以看出 print(add.name) 打印出来的函数名不是 add 而是 wrapper_func。
这是因为经过decorator()函数装饰后,add() 函数已经变成了:add = decorator(add)
而 decorator(add) 的返回值是 wrapper_func。

所以为了使 print(add.name) 能打印出原来的函数名,就需要对它用@functools.wraps(wrapped) 进行包裹,
参数 wrapped 为需要读取原函数属性的函数名。
如下:
2、被 @functools.wraps()装饰的函数

import functools

def decorator(fun):
    
    @functools.wraps(fun)      # fun 为被装饰的函数
    def wrapper_func(*args):
        res = fun(*args)
        print(f'{fun.__name__} 函数的计算结果为:{res}')       
    return wrapper_func

@decorator
def add(x,y):
    return x y

add(4,5)
print(add.__name__)

运行结果为:

add 函数的计算结果为:9
add

functools.update_wrapper(wrapper, wrapped, **kargs)

功能和 @functools.wraps() 一样,只是使用方式有点不同。
1、@functools.wraps(wrapped) 只要传入一个 被装饰 的函数就可以了。
2、functools.update_wrapper(wrapper, wrapped)需要传入装饰函数和被装饰的函数

import functools


def decorator(fun):
    
    #@functools.wraps(fun) 
    def wrapper_func(*args):
        res = fun(*args)
        print(f'{fun.__name__} 函数的计算结果为:{res}')
       
    return functools.update_wrapper(wrapper_func, fun)    # 使用方式。

@decorator
def add(x,y):
    return x y

add(4,5)
print(add.__name__)

7、 @functools.total_ordering

这是一个修饰 的装饰器。
用于自动生成比较方法,例如 > 、<、 = ……
但有个前提是,被修饰的这个类必须有下面四个方法中的一个 :

__lt__() 、__le__()、__gt__() 、 __ge__()

另外:最好 自己定义好 __ eq __() 方法,因为如果自己不定义这个方法,那么程序就会从 父类 object 中调用,从而导致 结果有偏差。

注意: 只有自己定义了上面四个方法中的一个,和 __eq__() 方法,total_ordering才会正确的生成其他的比较方法。

方法说明:

__eq__() : equal        等于     =
__ne__() : not equal    不等于  != 
__lt__() : less than    小于      < 
__le__() : less equal   小于等于  <=
__gt__() : great than   大于      >
__ge__() : great equal  大于等于 >=

例:有一个 People 的类,它含有 姓名年龄 两个属性。实例化后,可以在每个实例之间进行比较,看哪个年龄比较大。

from functools import *

@total_ordering
class People:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
    def __repr__(self):
        return f'名字:{self.name },年龄:{self.age}' 
    
    # 给比较函数调用,用来判断两个实例是否可比较。
    def _is_valid_operand(self, other):
        if hasattr(other, "name") and hasattr(other, 'age'):
            return True
        else:
            return False
        # 因为 hasattr() 函数返回的是 bool 值,所以可以直接用下面的代码替代上面的 if 语句。
        # return hasattr(other, "name") and hasattr(other, 'age')

    # 如果自定义 __eq__ 函数,那么调用 == 时,程序就会从 object 中调用__eq__ 函数。
    def __eq__(self, other):
        print('调用了 = 判断函数。')
        if not self._is_valid_operand(other):    # 找不到 name 和age 属性 则不能比较。
            print('根据 _is_valid_operand 判断后,两个实例无法比较。')
            return NotImplemented
        else:
            # 根据name和age是否相等,判断是不是同一个人。(名字都转成小写比较)
            if self.name.lower()  == other.name.lower() and self.age == other.age:
                return True
            else:
                return False
                
            # 因为 比较函数返回的是 bool 值,所以可以直接用下面的代码替代上面的 if 语句。
            # return self.name.lower()  == other.name.lower() and self.age == other.age
            
    def __lt__(self, other):
        print('调用了 < 判断函数。')
        if not self._is_valid_operand(other):    # 找不到 name 和age 属性 则不能比较。
            print('根据 _is_valid_operand 判断后,两个实例无法比较。')
            return NotImplemented
        else:
            # 根据age判断年龄大小。
            if self.age < other.age:        
                return  True
            else:
                return False


    
# 打印被装饰之后的People类中的__gt__方法
print(People.__gt__)
print(People.__eq__)
print(People.__lt__)

lilei = People('李雷',  12)
hanmeimei_1 = People('韩梅梅', 16)
hanmeimei_2 = People('韩梅梅', 16)

print(lilei > hanmeimei)
print(hanmeimei_1 == hanmeimei_2)

上面 People 类只定义了一个 等于小于函数,通过 @total_ordering 修饰后,自动生成了其他的比较函数。
另外: 虽然此装饰器使得创建具有良好行为的完全有序类型变得非常容易,但它 确实 是以执行速度更缓慢和派生比较方法的堆栈回溯更复杂为代价的。 如果性能基准测试表明这是特定应用的瓶颈所在,则改为实现全部六个富比较方法应该会轻松提升速度。

8、@functools.singledispatch

官网解释为:将一个方法转换为 单分派 generic function。
generic function :即 泛型函数 ,它为不同的类型实现相同操作的多个函数所组成的函数。在调用时会由调度算法来确定应该使用哪个实现。

通俗讲:
  当有一个函数需要根据传入的变量的类型来判断需要输出的内容时,通常的做法是在函数内部使用大量的if/elif/else来解决问题。这样做会使代码显得笨重,难以维护,也不便于扩展。

这时使用 @functools.singledispatch 它会根据传入的 第一个参数的类型,选择对应的函数,并把相同的参数传递给对应的函数,让那个函数去处理。

使用方法:

import functools

""" 第一步:先对需要 参数判断 的函数用 @functools.singledispatch 进行修饰。"""
@functools.singledispatch
def test(x, y):
    print(f'初始函数。')

""" 第二步:把 对应参数类型 要调用的函数注册到 test()函数中。"""
# 注册函数方法1:用装饰器注册。
@test.register(int)                 # 同 test.register(float, fun_int ), 见下方函数注册方法二。
def fun_int(x, y):
    res = x + y
    print(f'这是加法函数,值为:{res}')

# 第一个参数为 int 型,它会调用 fun_int(x, y) 函数。
test(2,3)        # >>> 这是加法函数,值为:5


def aaaa(x, y):
    print(f'调用了 aaaa 函数')

# 注册函数方法 2:用函数注册。
test.register(type(None), aaaa)       # 注册一个参数为 None 类型的参数。
# 第一个参数为 None 型,它会调用 aaa(x, y) 函数。
test(None,3)           # >>> 调用了 aaaa 函数


# 注册函数方法 3:可以叠加修饰。例如下面的,可看成:当传入的第一个参数是 浮点型 或 复数型。
@test.register(complex)
@test.register(float)
def fun_float(x, y):
    print(f'调用叠加注册函数,参数类型为 {type(x)}')

test(2.2, 3)            # >>> 调用叠加注册函数,参数类型为 

test(2.2+1j, 3)        # >>> 调用叠加注册函数,参数类型为 


""" 可以用 test.dispatch(cls) 查看 test 函数第一个参数为 某种类型时的 指向函数是什么函数。"""
# 查看 test 函数第一个参数为 int 类型时的 指向函数。
f = test.dispatch(int)
print(f.__name__)
f(2,3)

# 获取test函数为int类型绑定的函数
f = test.registry[float]
print(f.__name__)                # 获取函数名。
f(5,6)                           # 执行函数。

# 获取test函数所绑定的全部类型
print(test.registry.keys())

你可能感兴趣的:(python知识点整理,python,函数式编程)