functools 模块应用于高阶函数,即参数或(和)返回值为其他函数的函数,如装饰器、sorted函数的key参数等。通常来说,此模块功能适用于所有可调用对象。

https://docs.python.org/zh-cn/3/library/functools.html

发现functools里面有几个有意思的函数,记录一下:

cmp_to_key

将(旧式的)比较函数转换为新式的 key function . 在类似于 sorted()min()max()heapq.nlargest()heapq.nsmallest()itertools.groupby() 等函数的 key 参数中使用。此函数主要用作将 Python 2 程序转换至新版的转换工具,以保持对比较函数的兼容。

比较函数意为一个可调用对象,该对象接受两个参数并比较它们,结果为小于则返回一个负数,相等则返回零,大于则返回一个正数。key function则是一个接受一个参数,并返回另一个用以排序的值的可调用对象。

实例:

使用sorted + cmp_to_key,实现逆序排序

from functools import cmp_to_key
def mycmp(a, b):
    return b - a
arr = [1,9,3,7,8,2]
arr = sorted(arr, key=cmp_to_key(mycmp))
print(arr)

输出:

[9, 8, 7, 3, 2, 1]

一般的 key function 只接受一个参数,使用cmp_to_key,可以实现自定义的排序逻辑,如上述,也可以实现其他逻辑,比较经典的是 leetcode-179最大数,要求我们将数字按照特定的规律排序,然后返回一个长的字符串,参考答案:

class Solution:
    def largestNumber(self, nums: List[int]) -> str:
        # 第一步:定义比较函数,把最大的放左边
        # 第二步:排序
        # 第三步:返回结果
        def compare(x, y): return int(y+x) - int(x+y)
        nums = sorted(map(str, nums), key=cmp_to_key(compare))
        print(cmp_to_key)
        return "0" if nums[0]=="0" else "".join(nums)

如上述的排序,也可以做一个自定义的排序逻辑,如:如果是偶数,排序在前面,奇数排序在后面,然后奇数和偶数各自排序:

def mycmp(a, b):
    # 排序函数
    if a % 2 == b % 2:
        # 同为奇数或者偶数,较小者排在前面
        return a - b
    elif a % 2 == 0:
        # a 为偶数,排在前面
        return 1
    else:
        # b为偶数,排在前面
        return -1

输出:

[2, 8, 1, 3, 7, 9]

partial

返回一个新的 partial对象,当被调用时其行为类似于 func 附带位置参数 args 和关键字参数 keywords 被调用。 如果为调用提供了更多的参数,它们会被附加到 args。 如果提供了额外的关键字参数,它们会扩展并重载 keywords。 大致等价于:

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

例子:比如封装int,构造一个二进制转10进制的函数:

from functools import partial
bin2dec = partial(int, base=2)
bin2dec.__doc__ = "Convert binary string to decimal int"
print(bin2dec("100100"))

输出:36

wrapps

这是一个便捷更新 装饰器wrapper的函数,一般情况下,函数使用装饰器包装过后,调用 func.__name__输出的是装饰器的wrapper名称,如:

def decorator(f):
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return wrapper
  
@decorator
def func():
    print('in func.')

print(func.__name__)

输出: wrapper

如果 decorator 加上了 wraps:

from functools import wraps
def decorator(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return wrapper
  
@decorator
def func():
    print('in func.')

print(func.__name__)

输出:func

其实这个wraps调用的是 functools.update_wrapper,可以写成这样:

from functools import update_wrapper
def decorator(f):
    def wrapper(*args, **kwargs):
        return f(*args, **kwargs)
    return update_wrapper(wrapper=wrapper, wrapped=f)

实际上用的是 partial(update_wrapper, wrapped=f),函数定义:

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

singledispatch

将一个函数转换为 单分派 generic function

需要注意的是,它是单分派的,即只能根据一个参数进行选择。

假设有这么一个需求:实现一个encoder,对不同类型的数据进行编码,我们有4种数据类型:date、list、dict,str。

如果不用singledispatch的话,我们应该是写成这样的:

import datetime as dt

def encode(obj):
    if isinstance(obj, dt.date):
        return obj.strftime("%Y/%m/%d")
    elif isinstance(obj, list):
        return ",".join(obj)
    elif isinstance(obj, dict):
        return ",".join([f"{k}={v}" for k, v in obj.items()])
    elif isinstance(obj, str):
        return obj

调用:

print(encode(dt.date(2022, 5, 9)))
print(encode(["hello", "world"]))
print(encode({"name": "mike", "age": 19}))
print(encode("pure string."))

输出:

2022/05/09
hello,world
name=mike,age=19
pure string.

可以看到,encoder正常使用,但是,如果我们有新的类型需要加入进来的话,就需要定义更多的类型,加入更多的elif语句,比较繁琐,引入singledispatch,我们可以改编成这样:

import datetime as dt
from functools import singledispatch

@singledispatch
def encode(obj):
    return obj

@encode.register
def _(obj:dt.date):
    return obj.strftime("%Y/%m/%d")

@encode.register
def _(obj:list):
    return ",".join(obj)

@encode.register
def _(obj:dict):
    return ",".join([f"{k}={v}" for k, v in obj.items()])

如上,我们使用了singledispatch重写了encoder,并注册了3个分发函数,对应的类型为:date、list、dict,当encoder接收到这3中类型的obj参数的时候,会自动分发到对应的函数中去。否则,会使用默认的encoder函数。

重新运行,输出与上述保持一致。

这样,我们需要添加特定的类型解析的时候,只需要使用 @encode.register 为其注册一个解析器即可。可以在编写服务端的时候,为特定的ORM模型编写JSON序列化泛型函数。

如果我们定义的函数,不使用类型标注,我们也同样可以向register显式中传递一个类型:

@encode.register(list)
def _(obj):
    return ",".join(obj)

也可以传递多个:

@encode.register(list)
@encode.register(dict)
def _(obj):
    return ",".join(obj)

我们可以使用 registry属性,访问singledispatcher都有注册哪些函数:

print(encode.registry.items())
# 输出:dict_items([(<class 'object'>, <function encode at 0x7f8f53b934c0>), (<class 'datetime.date'>, <function _ at 0x7f8f53b93280>), (<class 'list'>, <function _ at 0x7f8f53b93c10>), (<class 'dict'>, <function _ at 0x7f8f53b931f0>)])

其他: