四时宝库

程序员的知识宝库

python中的函数增强神器functools模块


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 <module>
        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

    注意:wrapsupdate_wrapper是专为装饰器函数所设计,而且强烈建议在定义装饰器时进行修饰

    (此处已添加圈子卡片,请到今日头条客户端查看)

    发表评论:

    控制面板
    您好,欢迎到访网站!
      查看权限
    网站分类
    最新留言
      友情链接