python高阶函数库 functools使用指南,带你认识缓存、键函数、描述器、富比较方法、单派发和泛型函数

python高阶函数库 functools使用指南,带你认识缓存、键函数、描述器、富比较方法、单派发和泛型函数


functools 库用于提供高阶函数:作用于或返回其他函数的函数。一般,任何可调用对象都可以作为此模块的函数处理。
其中会涉及部分新的概念,包括:缓存、键函数、偏函数、描述器、富比较方法、单派发、泛型函数等


functools库提供函数一览表:

函数/装饰器 描述 备注
@cache(user_function) 为函数提供轻量级的缓存功能。即在调用时,如果用户输入的参数在之前已经计算过相同参数的结果,则直接返回计算结果,不必再次计算 其功能与 lru_cache(mzxsize=None)方法相同,都是创建一个查找参数的字典的简单包装器。但是它不需要移出旧值,所以比带有大小限制的 lru_cache() 更小更快。
@cached_property(func) 将类中的一个方法转换为属性,同时该属性的值仅计算一次,然后缓存为该示例生命周期中的常规属性。 该方法相当于带缓存功能的@property 装饰器。不同的是,@property如果不定义setter的话会使属性变为只读属性,但是cached_property是允许写入的。 对于实例中属性计算代价大的情况非常用。可以使用@cache和@property组合实现类似的效果(更节省空间)。
cmp_to_key(func) 将(旧式的)比较函数转换为新式的 key function。在类似于 sorted() ,min() ,max() ,heapq.nlargest() ,heapq.nsmallest() ,itertools.groupby() 等函数的 key 参数中使用。 此函数主要用作将 Python 2 程序转换至新版的转换工具,以保持对比较函数的兼容。 旧式比较函数是一个可调用对象,他接收两个参数并比较他们,结果为小于时返回一个负数,等于时返回0,大于时返回一个正数。key function则是一个接收一个参数,并返回另一个用于排序的值的可调用对象。
@lru_cache(user_function)和@lru_cache(maxsize=128,type的False) 一个为函数提供缓存功能的装饰器,缓存最近 maxsize 次的调用参数和结果。在下次以相同参数调用时直接返回上一次的结果,用以节约高开销或I/O函数的调用时间。如果max_size设为None,则会忽略lru算法,缓存可以无限增长。 由于使用了字典存储缓存,所以该函数的固定参数和关键字参数必须是可哈希的。不同模式的参数可能被视为不同从而产生多个缓存项,例如, f(a=1, b=2) 和 f(b=2, a=1) 因其参数顺序不同,可能会被缓存两次。lru是最久未使用淘汰算法。
@total_ordering 对于一个自定义了一个或多个富比较排序方法(rich comparison ordering methods)的类,这个类装饰器自动提供了其余的富比较方法。这简化了指定所有可能的富比较操作所涉及的工作。 该类必须定义__lt__(), __le__(), __gt__()或__ge__()中的一个方法。此外该类必须提供__eq__()方法的实现。注意:使用该装饰器可能会拖累类的性能表现,如果出现性能瓶颈,可以自行实现6个富比较方法。
partial(func,/,*args,**kwargs) 偏函数,返回一个partial对象。该对象在调用时的行为类似于使用位置参数args和关键字参数kwargs调用的func。如果向调用提供了更多参数,则会将其附加到参数。如果提供了其他关键字参数,它们将扩展和覆盖关键字。 偏函数常用于派生一个原函数部分入参固定的新函数,从而简化原函数的使用。
class partialmethod(func,/,*args,**kwargs) 返回一个新的partialmethod描述器,其行为类似于partial,不过它被设计用作方法定义而不是直接调用。 使用该方法首先需要了Python描述器的概念。不过在使用上用法和表现都与partial方法相似。
reduce(function, iterable[,initializer]) function方法需要包含两个参数,reduce会从左到右对iterable中的元素累积计算,得到一个计算结果。如果存在可选项 initializer,它会被放在参与计算的可迭代对象的条目之前,并在可迭代对象为空时作为默认值。 reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) 是计算 ((((1+2)+3)+4)+5) 的值。
@singledispatch 将一个函数转换为single-dispatch 泛型函数。相关规范参考PEP 443。在定义一个泛型函数时,使用该装饰器装饰它,分配调度发生在第一个参数的类型上。 泛函是为不同类型实现相同操作的多个函数所组成的函数。在调用时会由调度算法来确定应该使用那个类型的实现。如果实现代码根据单个参数的类型做出选择,则被称为单派发。
class singledispatchmethod(func) 将一个函数转换为单派发泛型函数。使用该装饰器时,单派发发生在第一个非self而非cls参数上。
update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) 更新一个 wrapper 函数以使其看起来像 wrapped 函数。 可选参数是元组,用于指定原始函数wrapped的哪些属性被直接分配给wrapper函数上匹配的属性,以及哪些wrapper函数的属性被更新为原始函数的相关属性。这些参数的默认值是模块级别的常量 WRAPPER_ASSIGNMENTS(分配给wrapper函数的 __module__,__name__,__qualname__,__annotations__ and __doc__文档字符串)和 WRAPPER_UPDATES (更新wrapper函数的 __dict__,即实例字典)。 为了允许出于内省和其他目的访问原始函数(例如, 绕过 lru_cache() 之类的缓存装饰器),此函数会自动为 wrapper 添加一个指向被包装函数的 __wrapped__ 属性。此函数的主要目的是在 decorator 函数中用来包装被装饰的函数并返回包装器wrapper。 如果包装器函数未被更新,则被返回函数的元数据将反映包装器定义而不是原始函数定义,这通常没有什么用处。
@wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) 这是一个便捷函数,用于在定义包装器函数时调用 update_wrapper() 作为函数装饰器。 它等价于 partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)。 如果不使用这个装饰器,对被装饰的函数查询名称就会是“ wrapper”(装饰器中返回的内层函数名称),原始被装饰函数example ()的文件字符串docstring就会丢失。

各类高阶函数使用详情:

1. 缓存

缓存是个 key : value 数据结构或数据库,用来加速用户访问的速度,当用户调用函数时,如果传入函数的参数与之前数据库中记录的 key 相同,则会直接返回库中存储的 value,而不必再次执行函数进行运算;如果传入的参数之前没存在库中,则执行函数,并将结果也存入库中。
不过要注意,调用的参数必须严格相同才会触发缓存查找,如3和3.0会被看做两个不同的key,f(a=1,b=2)和f(b=2, a=1) 也会看做是两个不同的key。

  1. @cache 代码示例:
    为函数提供轻量级的缓存功能。即在调用时,如果用户输入的参数在之前已经计算过相同参数的结果,则直接返回计算结果,不必再次计算。
    其功能与 lru_cache(mzxsize=None)方法相同,都是创建一个查找参数的字典的简单包装器。但是它不需要移出旧值,所以比带有大小限制的 lru_cache() 更小更快。

    # 不使用缓存
    def factorial(n):
        return n * factorial(n-1) if n else 1
    
    print(factorial(10))  # 3628800
    print(factorial.__dict__)  # {}
    
    # 使用缓存
    @cache
    def factorial_cache(n):
        return n * factorial(n-1) if n else 1
    
    print(factorial_cache(10))  # 3628800,没有缓存,执行11次递归计算
    print(factorial_cache(5))  # 120,直接从缓存中查找结果
    print(factorial_cache(12))  # 执行两次递归调用,剩余10次适用缓存结果
    
    print(factorial_cache.__dict__)  # {'cache_parameters': .decorating_function.. at 0x0000020835368160>, '__module__': '__main__', '__name__': 'factorial_cache', '__qualname__': 'factorial_cache', '__doc__': None, '__annotations__': {}, '__wrapped__': }
    # 查看缓存信息
    print(factorial_cache.cache_info())  # CacheInfo(hits=0, misses=3, maxsize=None, currsize=3)
    # 清空缓存
    factorial_cache.cache_clear()
    print(factorial_cache.cache_info())  # CacheInfo(hits=0, misses=0, maxsize=None, currsize=0)
    
  2. @cahce_property 代码示例:
    将类中的一个方法转换为属性,同时该属性的值仅计算一次,然后缓存为该示例生命周期中的常规属性。
    该方法相当于带缓存功能的@property 装饰器。不同的是,@property如果不定义setter的话会使属性变为只读属性,但是cached_property是允许写入的。
    对于实例中属性计算代价大的情况非常用。

    from functools import cached_property
    import statistics
    
    
    class DataSet(object):
        def __init__(self, nums) -> None:
            self._data = tuple(nums)
    
        @cached_property
        def stdev(self):
            """标准差"""
            return statistics.stdev(self._data)
    
    
    d = DataSet([1, 2, 3, 4, 5])
    print(d.stdev)  # 1.5811388300841898
    # 可以修改属性值
    d.stdev = 10  
    print(d.stdev)  # 10
    
    

    cached_property 装饰器仅在查找时运行,且仅在不存在相同名称的属性时运行。当它运行时,cached_property 写入具有相同名称的属性。后续属性的读写优先于 cached_property 方法,它的工作原理与普通属性类似。

    通过删除属性可以清除缓存的值。这允许 cached_property 方法再次运行。

    注意,该装饰器会干扰PEP 412 中key-share字典的操作,这意味着实力字典可能占用比平时更多的空间。
    另外,该装饰器要求每个实例上的__dict__是可变映射。这意味着他讲不适合某些类型,如metaclass(元类的__dict__属性是类命名空间中的只读代理)和那些指定了__slots__中不包含__dict__作为slots之一的类(因为这些类根本没有提供__dict__属性)。
    如果没有可变的映射可使用,或者需要空间高效的key-share,可以使用在@cache之上添加@property组合实现类似的效果。

    class DataSet:
        def __init__(self, sequence_of_numbers):
            self._data = sequence_of_numbers
    
        @property
        @cache
        def stdev(self):
            return statistics.stdev(self._data)
    
  3. @lru_cache(user_function)/@lru_cache(maxsize=128, typed=False) 代码示例:
    一个为函数提供缓存功能的装饰器,缓存最近 maxsize 次的调用参数和结果。在下次以相同参数调用时直接返回上一次的结果,用以节约高开销或I/O函数的调用时间。

    由于使用了字典存储缓存,所以该函数的固定参数和关键字参数必须是可哈希的。不同模式的参数可能被视为不同从而产生多个缓存项,例如, f(a=1, b=2) 和 f(b=2, a=1) 因其参数顺序不同,可能会被缓存两次。

    如果指定了 user_function,则它必须是可调用的。这使得 lru_cache 装饰器可以直接应用于用户函数,并将 maxsize 设为默认值128:

    @lru_cache
    def count_vowels(sentence):
        return sum(sentence.count(vowel) for vowel in 'AEIOUaeiou')
    

    如果max_size设为None,则会忽略lru算法,缓存可以无限增长。

    如果typed设置为 True,则将分别缓存不同类型的函数参数。如果类型为 false,则实现通常将它们视为等效调用,并且只缓存一个结果。(有些类型,例如 str 和 int,即使typed为 false,也可能单独缓存。)

    注意:类型特异性仅适用于函数的直接参数,而不适用于他们的内容(表达式结果)。如,标量参数 Decimal(42) 和 Fraction(42) 会被看做两个不同的结果的不同调用。相反的,元组参数 (‘answer’, Decimal(42)) 和 (‘answer’, Fraction(42))会被视为是等价的。

    被包装的函数使用 cache_parameters() 函数进行检测,该函数返回一个新的 dict,显示maxsize 和 typed 的值。这只是用来展示信息的,改变这两个值没有效果。

    为了帮助度量缓存的有效性并调优 maxsize 参数,使用 cache_info()函数检测包装函数,该函数返回一个named tuple,显示 hits, misses, maxsize 和 currsize

    装饰器还提供了一个 cache_clear ()函数,用于清除缓存或使其无效。

    原始的底层函数可以通过 __wrapped__ 属性来访问,这对于自省、绕过缓存或用不同的缓存重新包装函数都很有用。

    缓存保留对参数和返回值的引用,直到他们从缓存中过时或缓存被清除。

    当最近的调用是即将到来的调用的最佳预测时 (例如,新闻服务器上最流行的文章往往每天都在变化) LRU(least recently used)cache表现很好。缓存的大小限制确保了缓存不会在诸如 Web 服务器之类的长时间运行的进程上不受限制地增长。

    LRU指最近最少使用(least recently used),该算法是一种常用的页面值换算法,他会选择最近最少使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。

    通常,只有在想要重新使用以前计算的值时,才应使用LRU缓存。因此,缓存具有副作用的函数、需要在每次调用时创建不同可变对象的函数或不纯净的函数(如time() 或random() )是没有意义的。

    对静态网页内容使用LRU缓存的例子:

    @lru_cache(maxsize=32)
    def get_pep(num):
        'Retrieve text of a Python Enhancement Proposal'
        resource = 'https://www.python.org/dev/peps/pep-%04d/' % num
        try:
            with urllib.request.urlopen(resource) as s:
                return s.read()
        except urllib.error.HTTPError:
            return 'Not Found'
    
    >>> for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:  #其中存在访问相同网页的情况
    ...     pep = get_pep(n)
    ...     print(n, len(pep))
    
    >>> get_pep.cache_info()
    CacheInfo(hits=3, misses=8, maxsize=32, currsize=8)
    

    使用缓存来实现动态编程技术的高效计算斐波那契数的例子:

    @lru_cache(maxsize=None)  # 不使用LRU缓存
    def fib(n):
        if n < 2:
            return n
        return fib(n-1) + fib(n-2)
    
    >>> [fib(n) for n in range(16)]
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]
    
    >>> fib.cache_info()
    CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)
    

2. key functions

键函数或称排序规则函数(collation function)是一个可调用对象,它返回一个可用于sorting或ordering的值。例如,locale.strxfrm ()用于生成一个可以识别特定于语言环境的排序约定的排序键。

Python 中的许多工具接受键函数来控制元素的排序或分组方式。包括: min(), max(), sorted(), list.sort(), heapq.merge(), heapq.nsmallest(), heapq.nlargest(), and itertools.groupby().

创建键函数有多种方式,如,str.lower()方法可以作为不区分大小写排序的键函数。或者,可以从 lambda 表达式(如 lambda r: (r[0] ,r[2])构建键函数。此外, operator 模块还提供了三个键函数构造器: attrgetter(), itemgetter(), and methodcaller()
参考 Sorting HOW TO 学习如何创建和使用键函数。

cmp_to_key(func) 代码示例:

将(旧式的)比较函数转换为新式的 key function。在类似于 sorted() ,min() ,max() ,heapq.nlargest() ,heapq.nsmallest() ,itertools.groupby() 等函数的 key 参数中使用。此函数主要用作将 Python 2 程序转换至新版的转换工具,以保持对比较函数的兼容。
旧式比较函数是一个可调用对象,他接收两个参数并比较他们,结果为小于时返回一个负数,等于时返回0,大于时返回一个正数。key function则是一个接收一个参数,并返回另一个用于排序的值的可调用对象。

from functools import cmp_to_key
import locale

# locale-aware sort order
l = sorted(['你好', '大家好', 'hello', 'hello everyone'], key=cmp_to_key(locale.strcoll))
print(l)  # ['hello', 'hello everyone', '你好', '大家好']

local库实现国际化与本地化服务:https://docs.python.org/zh-cn/3.10/library/locale.html


3. 富比较方法

富比较方法(PEP 207)是Python基类object提供的一些列用于同类对象间进行比较的私有方法。当两个实例使用运算符<, ≤, >, ≥, ==, ! =或<>进行比较时就会调用对应的富比较方法。
即如下6个函数:

object.__lt__(self, other)  # 小于
object.__le__(self, other)  # 小于等于
object.__eq__(self, other)  # 等于
object.__ne__(self, other)  # 不等于
object.__gt__(self, other)  # 大于
object.__ge__(self, other)  # 大于等于

other表示要与之比较的对象。返回值最好是bool类型,但这是由开发者自行决定的,你可以返回任何想要的类型。

@total_ordering 代码示例:

对于一个自定义了一个或多个富比较排序方法(rich comparison ordering methods)的类,这个类装饰器自动提供了其余的富比较方法。这简化了指定所有可能的富比较操作所涉及的工作。
该类必须定义__lt__(), __le__(), __gt__()或__ge__()中的一个方法。此外该类必须提供__eq__()方法的实现。

@total_ordering
class Student:
    def _is_valid_operand(self, other):
        return (hasattr(other, "lastname") and
                hasattr(other, "firstname"))
    def __eq__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))

注意: 虽然此装饰器使得创建具有良好行为的完全有序类型变得非常容易,但它是以执行速度更缓慢和派生比较方法的堆栈回溯更复杂为代价的。 如果性能基准测试表明这是特定应用的瓶颈所在,则改为实现全部六个富比较方法应该会轻松提升速度。
注意:这个装饰器不会尝试重载本类或其父类中已经被声明的方法。这意味着如果某个上级类定义了比较运算符,则 total_ordering 将不会再次实现它,即使原方法是抽象方法。


4. 偏函数与描述器

偏函数:可以简单的理解为是对一个函数固定一部分参数后的二次封装。使用时,我们只需要传入剩余的不固定参数,就可以获得原函数的计算结果。他通过减少可变参数的数量,来降低函数的使用难度。

描述器:任何定义了__get__(), __set__(), 或__delete__() 方法的对象都是描述器。当一个类属性为描述器时,它的特殊绑定行为就会在属性查找时被触发。通常情况下,使用 a.b 来获取、设置或删除一个属性时会在 a 的类字典中查找名称为 b 的对象,但如果 b 是一个描述器,则会调用对应的描述器方法。理解描述器的概念是更深层次理解 Python 的关键,因为这是许多重要特性的基础,包括函数、方法、属性、类方法、静态方法以及对超类的引用等等。

更多关于描述器方法的信息,参考 Implementing Descriptors 或 Descriptor How To Guide。

  1. partial(func, /, *args, **kwargs) 代码示例:
    偏函数,返回一个partial对象。该对象在调用时的行为类似于使用位置参数args和关键字参数kwargs调用的func。如果向调用提供了更多参数,则会将其附加到参数。如果提供了其他关键字参数,它们将扩展和覆盖关键字。
    该方法可以理解为以下包装器:

    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
    

    代码示例:

    def add(a, b):
        return a + b
    
    add_five = partial(add, 5)  # 固定第一个参数为5
    print(add_five(3))  # 8,使用新函数时,只需要传入,第二个参数即可
    
    pow_2 = partial(pow, base=2)  # 也可以使用关键字参数
    pow_2.__doc__ = "计算2的n次方"
    
    print(pow_2(exp=10))  # 1024, 因为pow的第一个参数使用了关键字参数,所以第二个参数也得使用关键字参数传参
    
  2. class partialmethod(func, /, *args, **kwargs) 代码示例:
    返回一个新的partialmethod描述器,其行为类似于partial()方法,不过它被设计用作方法定义而不是直接调用。
    func 必须是一个描述器或可调用对象(与普通函数一样,二者都是作为描述器处理的对象)。

    func 是一个描述器(如,普通Python函数,classmethod(), staticmethod(), abstractmethod()或partialmethod的另一个实例)时,对 __get__() 的调用会被委托给底层的描述符,并且返回一个合适的偏函数作为结果。

    当 func 是一个 非描述器 的可调用对象时,将动态地创建合适的绑定方法。当作为方法使用时,它的行为与普通的 Python 函数相似: self 参数将作为第一个位置参数插入,甚至在提供给 partalmethod 构造函数的 args 和关键字之前。

    class Cell(object):
        def __init__(self) -> None:
            self._alive = False
    
        @property
        def alive(self):
            return self._alive
        
        def set_state(self, state):
            self._alive = bool(state)
    
        # 使用方法和partial方法类似
        set_alive = partialmethod(set_state, True)
        set_dead = partialmethod(set_state, False)
    
    c = Cell()
    print(c.alive)  # False
    c.set_alive()
    print(c.alive)  # True
    
  3. partial objects:
    partial对象是partial() 函数返回的可调用对象。该对象有三个只读属性:

    • partial.func:一个可调用的对象或函数。对partial对象的调用将带着新的位置参数和关键字参数被转发到 func 执行。
    • partial.args:最左边的位置参数,它将附加到提供给partial对象调用的位置参数之前。
    • partial.keywords:调用partial对象时将提供的关键字参数。

    partial对象像函数一样,它是可调用的,弱引用的,并且具有属性。也有一些重要的区别,如__name__ 和 doc 属性不是自动创建的。此外,类中定义的partial对象的行为类似于静态方法,在实例属性查找期间不会转换为绑定方法。


5. 单派发泛型函数

关于单派发泛型函数的规范定义在 PEP 443 中。

可以将二者拆分来看待:
泛型函数:是为不同类型实现相同操作的多个函数所组成的函数。在调用时会由调度算法来确定应该使用那个类型的实现。
单派发:如果泛型函数实现代码时是根据单个参数的类型做出选择,则被称为单派发。

  1. @singledispatch 代码示例:
    将一个方法转换为单派发泛型函数。要定义一个泛型函数,可以使用本装饰器来装饰它,单派发调度发生在第一个参数的类型上:

    @singledispatch
    def fun(arg, verbose=False):
        if verbose:
            print("let me hust say,", end=" ")
        print(arg)
    

    若要向函数添加重载实现,需要使用泛型函数的register() 属性,该属性可用作装饰器。对于带类型注释的函数,函数修饰符将自动推断出第一个参数的类型:

    @fun.register
    def _(arg: int, verbose=False):
        if verbose:
            print('Strength in numbers, eh?', end=" ")
        print(arg)
    
    
    @fun.register
    def _(arg: list, verbose=False):
        if verbose:
            print('Enumerate this:')
        for i, elem in enumerate(arg):
            print('(', i, elem, ')', end=" ")
    
    fun(2True)  # Strength in numbers, eh? 2
    fun(['a', 'b', 'c', 'd'])  # ( 0 a ) ( 1 b ) ( 2 c ) ( 3 d ) 
    

    可见,当对fun函数传入不同类型的参数时,fun会自动选择合适的方法来执行。
    对于不使用类型注释的代码,可以将适当的类型参数显式地传递给装饰器本身:

    @fun.register(complex)
    def _(arg, verbose=False):
        if verbose:
            print('Better than complicated.', end=" ")
        print(arg.real, arg.imag)
    
    fun(1+2j, True)  # Better than complicated. 1.0 2.0
    

    为了能够注册 lambda匿名函数和预先存在的函数,register ()属性也可以用于函数形式:

    def nothing(arg, verbose=False):
        print('Nothing.')
    
    fun.register(type(None), nothing)
    
    fun(None)  # Nothing.
    

    register() 属性返回未装饰的函数。这使得装饰器能够独立地堆叠stacking、pickle序列化pickling和创建creation 每个变体的单元测试:

    @fun.register(float)
    @fun.register(Decimal)
    def fun_num(arg, verbose=False):
        if verbose:
            print('Half of your number:', end=" ")
        print(arg / 2)
    
    fun(10.0, True)  # Half of your number: 5.0
    fun_num(20.0)  # 10.0
    print(fun is fun_num)  # False,虽然fun和fun_num接收浮点型参数时功能相同,但不是同一个函数
    

    当调用时,泛型函数分派第一个参数的类型:

    fun('Hello, world.')  # Hello, world.
    fun("test.", verbose=True)  # let me hust say, test.
    fun(42, verbose=True)  # Strength in numbers, eh? 42
    fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)  # Enumerate this: ( 0 spam ) ( 1 spam ) ( 2 eggs ) ( 3 spam )
    fun(None)  # Nothing.
    fun(1.23)  # 0.615
    

    如果没有特定类型的注册实现,则使用其方法解析顺序来查找更通用的实现。用@singledispatch 修饰的原始函数注册为基本 object 类型,这意味着如果没有更好的实现,就会使用它。
    如果一个实现被注册到一个抽象基类,那么基类的虚拟子类将被分派到该实现:

    from collections.abc import Mapping
    
    @fun.register
    def _(arg: Mapping, verbose=False):
        if verbose:
            print('keys & values')
        for key, value in arg.items():
            print(key, '=>', value)
    
    fun({"a": "b"})  # a => b
    

    要检查泛型函数将为给定类型选择哪个实现,可以使用dispatch()属性:

    print(fun.dispatch(float))  # 
    print(fun.dispatch(dict))  # 
    

    要访问所有已注册的实现,请使用只读registry属性,这是一个字典:

    print(fun.registry.keys())
    # dict_keys([, , , , , , , ])
    print(fun.registry[float])  # 
    print(fun.registry[object])  # 
    
  2. class singledispatchmethod(func) 代码示例:
    将一个方法转换为单派发泛型函数。要定义一个泛型方法,可以使用@singlepatchmethod 装饰器对其进行装饰。当使用@singlepatchmethod 定义函数时,请注意调度发生在第一个non-self 或 non-cls 参数的类型上:

    class Negator(object):
        @singledispatchmethod
        def neg(self, arg):
            raise NotImplementedError('Cannot negate a %s ' % type(arg).__name__)
        
        @neg.register
        def _(self, arg: int):
            return -arg
    
        @neg.register
        def _(self, arg: bool):
            return not arg
      
    n = Negator()
    print(n.neg(10))  # -10
    print(n.neg(True))  # False
    print(n.neg(10.0))  # NotImplementedError: Cannot negate a float 
    

    @singledispatchmethod支持与其他装饰器(如@classmethod)嵌套使用。**注意:**为了允许dispatcher.register singledispatchmethod必须是最外层的装饰器。
    以下是Negator类,其neg方法绑定到该类,而不是该类的实例:

    class Negator(object):
        @singledispatchmethod
        @classmethod
        def neg(cls, arg):
            raise NotImplementedError('Cannot negate a %s ' % type(arg).__name__)
        
        @neg.register
        @classmethod
        def _(cls, arg: int):
            return -arg
    
        @neg.register
        @classmethod
        def _(cls, arg: bool):
            return not arg
    
    print(Negator.neg(2))  # -2
    print(Negator.neg(False))  # True
    print(Negator.neg(2.0))  # NotImplementedError: Cannot negate a float 
    

    相同的模式可以用于其他类似的装饰器:@staticmethod、@abstractmethod和其他装饰器。


6. 包装器/装饰器

装饰器是Python中常用的语法糖,其本质是一个返回其他函数的函数。它让我们在不改变原函数使用方法和各项属性的情况下为原函数添加额外功能。

  1. update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
    更新wrapper函数,使其看起来像wrapped函数。可选参数是元组,用于指定原始函数的哪些属性直接分配给包装函数wrapper上的匹配属性,以及哪些包装函数wrapper属性被更新为原始函数的相应属性。这些参数的默认值是模块级别的常量 WRAPPER_ASSIGNMENTS(分配给wrapper函数的 __module__*,__*name__*,__*qualname__*,__*annotations__ and __doc__文档字符串)和 WRAPPER_UPDATES (更新wrapper函数的 __dict__,即实例字典)。

    为了自省和其他目的(例如绕过缓存修饰符,比如 lru_cache ())允许访问原始函数,这个函数会自动向包装器wrapper添加一个 __wrapped__属性,该属性会引用被包装函数wrapped。

    此函数的主要用途是在装饰器函数中,该函数包装装饰后的函数并返回包装器。如果包装器函数未更新,则返回函数的元数据将反射包装器定义,而不是原始函数定义,这通常没有多大帮助。

    update_wrapper() 还可以与处函数外的其他可调用对象一起使用。被包装对象中缺少的任何在参数“已赋值“ assigned 或“已更新” updated 中命名的属性都会被忽略(也就是说,这个函数不会尝试在包装函式上设置它们)。如果包装函数本身缺少updated中指定的任何属性,则仍会引发AttributeError。

  2. @wraps(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) 代码示例:
    这是一个便捷函数,用于在定义包装器函数时调用 update_wrapper() 作为函数装饰器。 它等价于 partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)

    from functools import wraps
    
    
    def my_decorator(f):
        """不使用warps的装饰器"""
        def wrapper(*args, **kwargs):
            print('Calling decorated function')
            return f(*args, **kwargs)
        return wrapper
    
    
    def my_wrapper_decorator(f):
        """使用warps的装饰器"""
        @wraps(f)
        def wrapper(*args, **kwargs):
            print('Calling decorated function')
            return f(*args, **kwargs)
        return wrapper
    
    
    @my_decorator
    def example(*args, **kwargs):
        """Unwrapped docstring"""
        print('Called example function. Args: %s, kwargs: %s' % (args, kwargs))
    
    
    @my_wrapper_decorator
    def example_wrapped(*args, **kwargs):
        """Wrapped docstring"""
        print('Called example function. Args: %s, kwargs: %s' % (args, kwargs))
    
    
    example(1, 2, 3, a=1, b=2)
    # Calling decorated function
    # Called example function. Args: (1, 2, 3), kwargs: {'a': 1, 'b': 2}
    print(example.__name__)  # wrapper
    print(example.__doc__)  # None
    
    example_wrapped(1, 2, 3, a=1, b=2)
    # Calling decorated function
    # Called example function. Args: (1, 2, 3), kwargs: {'a': 1, 'b': 2}
    print(example_wrapped.__name__)  # example_wrapped
    print(example_wrapped.__doc__)  # Wrapped docstring
    

    如果不使用这个带wrpas方法的装饰器工厂,虽然,被装饰函数是想的功能完全一样,但是示例函数的名称就会是“ wrapper”,原始 example()的 docstring 就会丢失。


参考文档

functols文档: https://docs.python.org/3/library/functools.html
PEP 443:https://www.cnblogs.com/popapa/p/PEP443.html

你可能感兴趣的:(python,python,缓存,富比较方法,单派发,泛型函数)