前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >python中的函数增强神器functools模块

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

作者头像
星星在线
发布2020-05-22 15:13:44
1.2K0
发布2020-05-22 15:13:44
举报

functools是一个函数增强器,主要为高阶函数使用,作用于或者返回其他函数的函数,通常任何可调用的对象都可视为“函数”。主要包括以下几个函数:

cached_property

将类的方法转换为属性,该属性的值将被计算一次,然后在实例生命周期中作为常规属性进行缓存。与property()类似,但增加了缓存,对于计算复杂的属性很有用。cached_property在Python3.8之前的很多第三方库当中都有自己的实现,比如werkzeug.utils.cached_property、django.utils.functional.cached_property

举例如下:

代码语言:javascript
复制
# 在没有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

使用变量记录属性数据,并在属性计算时进行判断,防止计算多次

代码语言:javascript
复制
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转换而来的程序的转换工具,该程序支持使用比较函数。

比较函数是任何可调用的函数,它们接受两个参数进行比较,小于返回一个负数,等于返回零,大于返回一个正数。键函数是一个可调用的函数,它接受一个参数并返回另一个值用作排序键。

代码语言:javascript
复制
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)将被视为具有不同结果的不同调用;
  • 缓存是有内存存储空间限制的;
代码语言:javascript
复制
def a(x):
    print(x)
    return x+1

print(a())
# 输出:
3
4

print(a())
# 输出:
3
4

不使用缓存记录,每次都重新执行函数计算

代码语言:javascript
复制
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

偏函数,可以扩展函数功能,但是不等于装饰器,通常应用的场景是当我们要频繁调用某个函数时,其中某些参数是已知的固定值,通常我们可以调用这个函数多次,但这样看上去似乎代码有些冗余,而偏函数的出现就是为了很少的解决这一个问题。

举一个简单的例子:

代码语言:javascript
复制
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

代码语言:javascript
复制
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

在进行函数重新定义时,如果需要固定非关键字参数,那么默认定义的是第一个非关键字参数;如果需要固定关键字参数,直接指定关键字即可。

实际上偏函数的使用更多是在回调函数时使用,举例如下:

代码语言:javascript
复制
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

上面我们在注册回调函数的时候,需要记录函数名和各个参数,非常不方便,如果使用偏函数进行修饰

代码语言:javascript
复制
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

函数原型如下:

代码语言:javascript
复制
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函数进行累计计算,并将最终值返回。一个简单的使用示例:

代码语言:javascript
复制
a=[1,3,5]
b=reduce(lambda x,y:x+y,a)
print(b)
# 输出
9

将a列表传入匿名函数进行累加计算

singledispatch

python函数重载,直接举例来说明

代码语言:javascript
复制
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也提供了这样的重载方式

代码语言:javascript
复制
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__()方法。简单来说就是只需要重载部分运算符,装饰器就会自动帮我们实现其他的方法。

代码语言:javascript
复制
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装饰器修饰以后。

代码语言:javascript
复制
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中的装饰器是“接受函数为参数,以函数为返回值”。但是装饰器函数也会有一些负面影响。我们来看一下例子:

代码语言:javascript
复制
# 普通函数
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就为我们解决了这个问题。

代码语言:javascript
复制
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) 的函数源码为:

代码语言:javascript
复制
def wraps(wrapped, assigned = WRAPPER_ASSIGNMENTS, updated = WRAPPER_UPDATES):
    return partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)

使用方式:

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 python爬虫实战之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • cached_property
  • cmp_to_key
  • lru_cache
  • partial
  • reduce
  • singledispatch
  • total_ordering
  • wraps
  • update_wrapper
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档