百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

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

toyiye 2024-09-02 02:27 3 浏览 0 评论


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是专为装饰器函数所设计,而且强烈建议在定义装饰器时进行修饰

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

    相关推荐

    # Python 3 # Python 3字典Dictionary(1)

    Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值(key=>value)对用冒号(:)分割,每个对之间用逗号(,)分割,整个字典包括在花括号({})中,格式如...

    Python第八课:数据类型中的字典及其函数与方法

    Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值...

    Python中字典详解(python 中字典)

    字典是Python中使用键进行索引的重要数据结构。它们是无序的项序列(键值对),这意味着顺序不被保留。键是不可变的。与列表一样,字典的值可以保存异构数据,即整数、浮点、字符串、NaN、布尔值、列表、数...

    Python3.9又更新了:dict内置新功能,正式版十月见面

    机器之心报道参与:一鸣、JaminPython3.8的热乎劲还没过去,Python就又双叒叕要更新了。近日,3.9版本的第四个alpha版已经开源。从文档中,我们可以看到官方透露的对dic...

    Python3 基本数据类型详解(python三种基本数据类型)

    文章来源:加米谷大数据Python中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。在Python中,变量就是变量,它没有类型,我们所说的"类型"是变...

    一文掌握Python的字典(python字典用法大全)

    字典是Python中最强大、最灵活的内置数据结构之一。它们允许存储键值对,从而实现高效的数据检索、操作和组织。本文深入探讨了字典,涵盖了它们的创建、操作和高级用法,以帮助中级Python开发...

    超级完整|Python字典详解(python字典的方法或操作)

    一、字典概述01字典的格式Python字典是一种可变容器模型,且可存储任意类型对象,如字符串、数字、元组等其他容器模型。字典的每个键值key=>value对用冒号:分割,每个对之间用逗号,...

    Python3.9版本新特性:字典合并操作的详细解读

    处于测试阶段的Python3.9版本中有一个新特性:我们在使用Python字典时,将能够编写出更可读、更紧凑的代码啦!Python版本你现在使用哪种版本的Python?3.7分?3.5分?还是2.7...

    python 自学,字典3(一些例子)(python字典有哪些基本操作)

    例子11;如何批量复制字典里的内容2;如何批量修改字典的内容3;如何批量修改字典里某些指定的内容...

    Python3.9中的字典合并和更新,几乎影响了所有Python程序员

    全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

    Python3大字典:《Python3自学速查手册.pdf》限时下载中

    最近有人会想了,2022了,想学Python晚不晚,学习python有前途吗?IT行业行业薪资高,发展前景好,是很多求职群里严重的香饽饽,而要进入这个高薪行业,也不是那么轻而易举的,拿信工专业的大学生...

    python学习——字典(python字典基本操作)

    字典Python的字典数据类型是基于hash散列算法实现的,采用键值对(key:value)的形式,根据key的值计算value的地址,具有非常快的查取和插入速度。但它是无序的,包含的元素个数不限,值...

    324页清华教授撰写【Python 3 菜鸟查询手册】火了,小白入门字典

    如何入门学习python...

    Python3.9中的字典合并和更新,了解一下

    全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

    python3基础之字典(python中字典的基本操作)

    字典和列表一样,也是python内置的一种数据结构。字典的结构如下图:列表用中括号[]把元素包起来,而字典是用大括号{}把元素包起来,只不过字典的每一个元素都包含键和值两部分。键和值是一一对应的...

    取消回复欢迎 发表评论:

    请填写验证码