Python中的高阶函数以及柯里化、functools模块、lru_cache实现

1. 高阶函数和柯理化

1.1. 高阶函数(High-Order Function)

一个函数要成为高阶函数,需要满足下面至少一个条件:

  1. 函数定义中接收一个或者多个函数作为参数
  2. 返回值为函数

满足上述两点条件的任意一点,即可将该函数称为高阶函数。

Python中内建了一些高阶函数,比如sorted,比如min这类可以接收函数作为参数的内建函数;另外,比如偏函数functools.partial,这个函数接收函数作为参数,同时将函数作为返回值。函数装饰器通常都属于高阶函数,装饰器既需要函数作为参数,也需要将函数作为返回值。

下面实现一个自定义的高阶函数。具体代码如下所示:

def echo_hw(arg):
    print('Hello World! This is {}'.format(arg))
    
def high_order_func(func, arg):
    func(arg)
    print('Welcome to Python!')

high_order_func(echo_hw, 'Python')  

上述代码的输出结果如下所示:

Hello World! This is Python
Welcome to Python!

上述代码并没有什么实际意义,只是一种定义高阶函数的方式——接收函数作为参数的函数,可以被称为是高阶函数。

将上述代码稍作改变,使其返回值为参数——也可以被称为是高阶函数。具体代码如下所示:

def high_order_func(arg):
    def echo_hw():
        print('Hello World! This is {}'.format(new_arg))
    new_arg = arg + '\nWelcome to Python!'
    return echo_hw

ret_func = high_order_func('Python')
ret_func()

上述函数的输出结果如下所示:

Hello World! This is Python
Welcome to Python!

上述函数代码同样没有什么实际意义,只是说明如果一个函数的返回值是函数,那么这个函数也可以被称为是高阶函数。

上面的两种代码实现方式虽然有差异,但是实现的目的是相同的,且都符合高阶函数的构成条件,所以都是高阶函数。

1.2. 柯里化(Currying)

而所谓的柯里化(Currying),则是指,将接收多个参数的函数转换为单参函数,并将剩余的参数封装在该单参函数内的嵌套函数中,作为嵌套函数的参数(嵌套函数可以接收所有剩余的参数,也可以接收剩余参数中的一个或者几个,并将此后剩余的参数再继续构建嵌套函数)。通过嵌套函数以及闭包作用域,将多参函数的参数拆解到多个嵌套函数中。柯里化的本质是嵌套函数以及闭包作用域下的变量查询。柯里化是实现函数装饰器的一个手段。

下面通过示例,说明如何将一个普通函数进行柯里化改稿的过程。

一个多参普通函数,如下所示:

def multi_add(a, b, c):
 return a + b + c

res = multi_add(1, 2, 3)
print(res)

上述函数的输出结果为6。

将上述普通函数通过柯里化改造为func(a)(b, c)的形式。

改造代码具体如下所示:

def multi_add_cury(a):
 def wrapper(b, c):
     return a + b + c
 return wrapper

ret_func = multi_add_cury(5)
res = ret_func(6, 7)
print(res)

上述即为柯里化之后的函数,需要进行两次函数调用(伴随着两次参数传递过程)才能获得最终的执行结果,其执行结果为18。

将上述普通函数通过柯里化改造为func(a)(b)(c)的形式。

改造代码具体如下所示:

def multi_add_cury1(a):
 def wrapper(b):
     def inner(c):
         return a + b + c
     return inner
 return wrapper

wrap_func = multi_add_cury1(7)
inner_fun = wrap_func(8)
res = inner_fun(9)
print(res)

上述即为柯里化之后的函数,需要进行三次函数调用(伴随着三次参数传递过程)才能获得最终的执行结果。其计算结果为24。

最后还可以将普通函数通过柯里化改造为func(a, b)(c)的形式。

改造代码具体如下所示:

def multi_add_cury2(a, b):
 def wrapper(c):
     return a + b + c
 return wrapper

wrap_func = multi_add_cury2(11, 12)
res = wrap_func(13)
print(res)

上述即为柯里化之后的函数,需要经过两次函数调用(伴随两次参数传递过程)才能获得最终的计算结果。上述执行结果为36。

上述即为函数柯里化的过程以及方式。

2. functools包内的每个函数的功能作用

functools模块中提供了一些高阶函数,用于实现用户自己的通用函数。在我安装的Python-3.7中,包含的方法个数为9个,分别如下所示:

  1. functools.cmp_to_key:这个高阶函数的作用是将老式的比较函数转换为新式的key形式函数,以便其可以用在接收key=key_func作为参数的函数(比如sorted(), min(), max(), heapq.nlargest()heapq.nsmallest(), itertools.groupby()等等这类函数)中。这类函数中的key参数通常要求指定的函数接收1个参数,所以就需要这个cmp_to_key高阶函数将传统的接受2个参数的比较函数转换为接受1个参数的key形式函数。所谓的比较函数,是指接收2个参数,并比较这两个参数的大小,如果是小于的关系,则返回负数;如果相等,则返回0;如果是大于关系,则返回正数。而key形式函数是一个可调用对象,其接受1个参数,并且返回另一个对象,使用这个返回的对象作为key=的参数值。

    这个函数的帮助基本信息如下所示:

    functools.cmp_to_key(func)
    

    下面通过sorted函数中的key=参数,示例这个functools.cmp_to_key高阶函数的使用方式。具体如下所示:

    from functools import lru_cache, cmp_to_key
    
    res = sorted([7, 2, 3, 1, 5, 8], key=cmp_to_key(lambda x, y: x - y))
    print(res)
    

    上述函数的执行结果如下所示:

    [1, 2, 3, 5, 7, 8]
    
    Process finished with exit code 0
    

    通过上述函数就完成了参数的转换。但是这个背后过程并不很清楚。

    下面再看两个关于sorted函数中不同的key=参数取值情况的排序表现。

    sorted函数的key=参数不适用functools.cmp_to_key高阶函数的时候,其排序表现如下所示:

    sorted([7, 3, 2, 1, 5, 6, 9, 8], key=lambda x: x)
    

    上述语句的执行结果如下所示:

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

    从上述结果中可以看出,对结果进行了排序,其实省略掉key=这个参数,也是默认按照升序排列的。接下来对key=的参数稍作调整,具体如下所示:

    sorted([7, 3, 2, 1, 5, 6, 9, 8], key=lambda x, y: x, y)
    

    此时执行报错,具体如下所示:

    File "C:\Users\ikkys\AppData\Local\Temp/ipykernel_187720/3200397783.py", line 1
     sorted([7, 3, 2, 1, 5, 6, 9, 8], key=lambda x, y: x, y)
                                                         ^
    SyntaxError: positional argument follows keyword argument
    

    上面的异常信息中提示,在关键字参数后面跟了一个位置参数。因为key参数要求指定的函数为单参函数,所以这里指定两个是不合适的。

    接下来使用functools.cmp_to_key这个高阶函数将接收两个参数的比较函数转换为单参函数。

    具体如下所示:

    sorted([7, 3, 2, 1, 5, 6, 9, 8], key=cmp_to_key(lambda x: x))
    

    上述的执行结果会报错,具体如下所示:

    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    ~\AppData\Local\Temp/ipykernel_187720/3510372626.py in <module>
    ----> 1 sorted([7, 3, 2, 1, 5, 6, 9, 8], key=cmp_to_key(lambda x: x))
    
    TypeError: <lambda>() takes 1 positional argument but 2 were given
    

    提示传给key=参数的lambda匿名函数只接收1个参数,但是调用的时候传递了两个参数。所以这也是为什么functools.cmp_to_key高阶函数的参数需要是一个接收2个参数的函数了。

    修正如下所示:

    sorted([7, 3, 2, 1, 5, 6, 9, 8], key=cmp_to_key(lambda x, y: x - y))
    

    上述的执行结果如下所示:

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

    从上述结果中可以看出,输入的列表中的元素按照升序被排列出来了。

    改一下lambda匿名函数,应该就可以实现降序排列了,具体如下所示:

    sorted([7, 3, 2, 1, 5, 6, 9, 8], key=cmp_to_key(lambda x, y: y - x))
    

    上述的执行结果如下所示:

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

    可以看出,将升序中的lambda函数体中的x-y改为y-x之后,就实现了降序排列。

  2. functools.lru_cache:这也是一个高阶函数,可以作为装饰器封装一个函数(即被修饰的函数),并对该函数的调用进行记录,最大记录数取决于参数maxsize指定的值(默认值为128)。对于周期性使用相同参数重复调用的过程,可以直接从记忆中返回结果,而无需重复执行函数内的语句,从而节省资源和时间开销。

    由于在这个高阶函数中使用了字典来记录被修饰函数的多次不同的调用产生的结果,所以给被修饰函数传递的位置参数以及关键字参数都必须是可哈希类型的才可以。

    参数形式上的差异,会作为不同的调用,所以缓存起来的结果虽然相同,但是调用参数的差异,并不会作为同一个项目存储在字典中。比如f(a=1, b=2)f(b=2, a=1)这两个函数调用的结果虽然相同,但是在缓存中并不相同,因为它们的关键字参数顺序不同,所以在缓存中以不同的项目被缓存起来了。

    如果给该高阶函数支持传入用户自定义函数作为参数,那么functools.lru_cache会直接装饰这个函数。这个函数的两个使用形式如下所示:

    @functools.lru_cache(user_function)
    

    或者

    @functools.lru_cache(maxsize=128, typed=False)
    

    上面的第一种形式中,可以接收一个函数作为参数。第二种形式中,接收两个参数,其中maxsize=128指定缓存个数;typed指定用于指定缓存结果是否区分数据类型。

    maxsize被设置为None的时候,表示对缓存大小不做限制,缓存的结果数可以不受约束的增长,直至耗尽内存。

    typed被设置为True的时候,不同类型的结果将会分别进行缓存。比如函数调用f(3)f(3.0)就是两个不同的缓存结果。

    关于该函数的使用示例,如下所示:

    下面以计算斐波那契数列为例,演示使用和不使用该函数的区别。

    当采用递归计算的方式,不使用functools.lru_cache作为装饰器的时候,代码如下所示:

    def fib(n):
     if n < 2:
         return n
     else:
         return fib(n - 1) + fib(n - 2)
    
    
    # Test fib(12)
    num = 40
    start = datetime.datetime.now()
    res1 = fib(num)
    delta = (datetime.datetime.now() - start).total_seconds()
    
    print('the {}\'s fibonacci result is {}. \nTake {} seconds'.format(num, res1, delta))
    

    上述的计算结果如下所示:

    the 40's fibonacci result is 102334155. 
    Take 20.872155 seconds
    

    计算40的斐波那契数值,耗时20.6秒。

    下面采用functools.lru_cache作为装饰器,修饰上述斐波那契额数列计算函数。具体如下所示:

    此时由于有缓存,所以计算结果很快就给出了。代码具体如下所示:

    @lru_cache(maxsize=128)
    def fib_deco(n):
     if n < 2:
         return n
     else:
         return fib_deco(n - 1) + fib_deco(n - 2)
    
    
    # Test fib(12)
    start_deco = datetime.datetime.now()
    res_deco = fib_deco(num)
    delta_deco = (datetime.datetime.now() - start_deco).total_seconds()
    
    print('the {}\'s fibonacci result is {}. \nTake {} seconds after decoration'.format(num, res_deco, delta_deco))
    
    

    上述的计算结果如下所示:

    the 40's fibonacci result is 102334155. 
    Take 0.0 seconds after decoration
    

    很短的时间就完成了。同样的计算方法,使用缓存和不适用缓存的结果差异很明显。

    在我的Python-3.7中,被functools.lru_cache修饰的函数并没有cache_info()这个函数,也没有cache_clear()这个函数。

  3. functools.partial:这也是一个高阶函数,并且是偏函数(姑且这么称呼吧,虽然在源码层面的实现上,这是一个类)。所谓的偏函数,是指将一个函数的某个或者某几个参数赋予默认值,并返回一个partial对象的一类函数。

    该函数的基本形式为:

    functools.partial(func, /, *args, **keywords)
    

    当调用该函数返回的partial对象的时候,其行为就像使用位置参数args和关键字参数keywords调用func函数一样。如果在调用的时候,提供了多个位置参数,那么这些位置参数会被收集到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
    

    从上面的代码中可以看出这个偏函数对传入的函数以及参数做了何种处理。该偏函数将收到的关键字参数与传给func函数的关键字参数进行组合,形成新的参数字典,并在定义newfunc的时候,将新的参数字典设置为func的关键字参数。而对于偏函数接收到位置参数,以及func接收到的位置参数,偏函数会将func的位置参数加在偏函数的位置参数后面,统一作为新的参数组合,传递给func函数。随后返回这个func函数。

    而在偏函数返回newfunc之前,会将这个函数的函数属性做一些修改(上述等效代码中的第5-7行的这3行代码,在这里具有一些迷惑性)。为此搜索了网上的一篇博客(参见Reference部分的第一条链接),记录了分析过程,在这篇博客中,作者将这三行注释掉,事实上依然可以正常工作。上述等效代码的写法,容易让人产生误解,即最终返回的newfunc的位置参数被替换为调用偏函数时的位置参数了,关键字参数被替换为了调用偏函数时的关键字参数了。但事实上并非如此。具体参见我的Python-3.7下的这个函数的部分源代码实现。具体如下面的代码所示:

    class partial:
     """New function with partial application of the given arguments
     and keywords.
     """
    
     __slots__ = "func", "args", "keywords", "__dict__", "__weakref__"
    
     def __new__(*args, **keywords):
         if not args:
             raise TypeError("descriptor '__new__' of partial needs an argument")
         if len(args) < 2:
             raise TypeError("type 'partial' takes at least one argument")
         cls, func, *args = args
         if not callable(func):
             raise TypeError("the first argument must be callable")
         args = tuple(args)
    
         if hasattr(func, "func"):
             args = func.args + args
             tmpkw = func.keywords.copy()
             tmpkw.update(keywords)
             keywords = tmpkw
             del tmpkw
             func = func.func
    
         self = super(partial, cls).__new__(cls)
    
         self.func = func
         self.args = args
         self.keywords = keywords
         return self
    
     def __call__(*args, **keywords):
         if not args:
             raise TypeError("descriptor '__call__' of partial needs an argument")
         self, *args = args
         newkeywords = self.keywords.copy()
         newkeywords.update(keywords)
         return self.func(*self.args, *args, **newkeywords)
    
    

    从上述代码的第18-24行中,已经对关键字参数进行了合并,同时也对偏函数调用的时候接受的参数与newfunc调用的时候接受的参数进行了合并。

    在解清了上述迷惑之后,看下这个函数是如何应用的。

    具体如下所示:

    def add_1(a, b, c, d, e):
     print(a, b, c, d, e)
     return a + b + c + d + e
    
    new_add_1 = partial(add_1, c=5, d=6, e=7)
    res_2 = new_add_1(3, 4)
    print(res_2)
    res_3 = new_add_1(3, 4, c=10, d=20, e=30)
    print(res_3)
    res_4 = new_add_1(2, 3, 4)
    print(res_4)
    

    上述代码的钱9行可以正常输出结果,具体如下所示:

    3 4 5 6 7
    25
    3 4 10 20 30
    67
    

    但是第10行的调用,明显是不符合偏函数中定义的要求的,所以代码执行到第10行的时候,会抛出异常。具体如下所示:

    Traceback (most recent call last):
    File "F:/PyCharm-Projects/Python-6th-week/decorator-test.py", line 153, in <module>
     res_4 = new_add_1(2, 3, 4)
    TypeError: add_1() got multiple values for argument 'c'
    

    至此,偏函数functools.partial基本就清楚了。这个函数本质上,相当于给被修饰函数绑定了一些属性,即给函数的部分属性绑定了默认值。

  4. functools.partialmethod:与functools.partial类似,也是一个类,此处仍然将其称为偏函数,其使用信息如下所示:

    class functools.partialmethod(func, /, *args, **keywords)
    

    与前面的functools.partial不同的是,这个偏函数返回一个partialmethod属性描述符(descriptor)对象,其行为与functools.partial类似,只是这个对象是被用来作为类方法而定义的,而不像partial是可以直接调用的。

    在上面的参数中,func必须是属性描述符(descriptor)或者可调用对象(符合这两点要求的对象,可以像常规函数一样作为属性描述符被处理)。

    当func是属性描述符的时候(比如正常的Python函数,classmethod(), staticmethod(), abstractmethod()或者partialmethod返回的实例对象),会调用隐式方法__get__(),从而被委派为属性描述符,同时返回一个合适的partial对象作为结果。

    当func不是属性描述符(descriptor),但是是可调用对象的时候,会动态创建一个合适的绑定方法。这个行为类似于将常规的函数作为类的方法函数使用一样:会在方法函数的第一个位置参数自动插入一个self,甚至是在提供给partialmethod的args位置参数以及keywords关键字参数之前。

    示例如下:

    from functools import partialmethod
    
    class Cell(object):
     def __init__(self):
         self._alive = False
     @property
     def alive(self):
         return self._alive
     def set_state(self, state):
         self._alive = state
     set_alive = partialmethod(set_state, True)
     set_dead = partialmethod(set_state, False)
    
    c = Cell()
    print(c.alive)
    c.set_alive()
    print(c.alive)
    c.set_dead()
    print(c.alive)
    

    上述代码的输出结果如下所示:

    False
    True
    False
    
  5. functools.reduce:该函数也是一个高阶函数,其使用形式如下所示:

    functools.reduce(function, iterable[, initializer ])
    

    该函数的作用是将function的两个参数累积起来,并且按照从左到右的顺序逐个应用到iterable的每个项目上,最终将iterable中的元素累积成一个值。比如下面的示例:

    from functools import reduce
    
    
    reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])
    

    上面的代码输出结果为15。reduce函数调用的等效表达式为:((((1+2)+3)+4)+5),即拿iterable中的前两项做加法计算,然后将得到的和与iterable中的第三项做加法运算,依此类推,直至将iterable中的所有项目计算完成。

    在上面的函数调用中,传递了一个匿名的lambda函数,该函数的x为被累积的项,y为每次都会从iterable对象中更新的项。

    如果在调用functools.reduce的时候给定了可选参数initializer,那么他就会被放在iterable的各个项之前计算,并且当iterable为空的时候作为默认值。如果没有给定initializer参数,同时iterable中只包含一项,那么这个函数调用则只是返回iterable中包含的这个项。

    functools.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
    

    上述等效代码中使用了迭代器(第2行),如果指定了initializer,并且不为None,那么初始值就是initializer;否则的话,初始值就是迭代器产生的第一个值。随后使用这个初始值于iterable中的剩下的各个项依次组合,作为参数传递给function函数,并将函数调用的结果再赋值给value,作为下次迭代的第一个参数,重复这个过程直到将iterable中的所有项都迭代完成为止。最终返回value作为reduce的调用结果。

  6. functools.singledispatch:该函数通常作为函数修饰符使用,其作用是将被修饰的函数转换为single-dispatch generic function,此处的generic function的含义是一个满足了多种不同数据类型的相同操作的通用函数,是一种重载(overload),就是对不同的数据类型对象可以提供相似行为的操作,而具体采取什么操作,取决于调用该函数的时候传递的数据类型(关于这个名词的官方文档解释内容为:generic function: A function composed of multiple functions implementing the same operation for different types. Which implementation should be used during a call is determined by the dispatch algorithm. )。此处所谓的single-dispatch,是指基于传递的参数数据类型选择与之相关的函数进行调用(single dispatch:A form of generic function dispatch where the implementation is chosen based on the type of a
    single argument.)。

    该函数的基本使用形式如下所示:

    @functools.singledispatch
    

    要创建一个重载函数,通常需要将这个函数作为该函数的装饰器,至于这个重载函数被调用的时候,会被投递到哪个具体的实现上,取决于调用的时候传递的第一个参数的数据类型。

    该函数的使用示例,具体如下所示:

    下面是一个完整的示例,实现了函数的重载。具体如下所示:

    from functools import singledispatch
    from decimal import Decimal
    
    
    @singledispatch
    def fun(arg, verbose=False):
     '''
     To define a generic function, decorate it with the @singledispatch decorator. 
     Note that the dispatch happens on the type of the first argument. 
     Create your function accordingly:
     '''
     if verbose:
         print("Let me just say,", end=' ')
     print(arg)
    
    
    '''
    To add overloaded implementations to the function, use the register() attribute 
    of the generic function. This is a decorator, taking a type parameter and 
    decorating a function implementing the operation for that type:
    ''' 
    @fun.register(int)
    def _(arg, verbose=False):
     if verbose:
         print("Strength in numbers, eh?", end=' ')
     print(arg)
    
    @fun.register(list)
    def _(arg, verbose=False):
     if verbose:
         print("Enumerate this: ")
     for i, elem in enumerate(arg):
         print(i, elem)
    
    
    '''
    To enable registering lambdas and pre-existing functions,
    the register() attribute can be used in a functional form:
    ''' 
    def nothing(arg, verbose=False):
     print('Nothing.')
    
    fun.register(type(None), nothing)
    
    
    '''
    The register() attribute returns the undecorated function. 
    This enables decorator stacking, pickling, as well as creating
    unit tests for each variant independently:
    '''
    @fun.register(float)
    @fun.register(Decimal)
    def fun_num(arg, verbose=False):
     if verbose:
         print("Half of your number: ", end=' ')
     print(arg / 2)
    
    print(fun_num is fun)      # False
    
    
    '''
    When called, the generic function dispatches on the 
    type of the first argument:
    '''
    fun("Hello, World!")       # Hello, World!
    fun("test", verbose=True)  # Let me just 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
    

    在上面的函数中,定义了一个重载函数,函数名为fun,该函数接收2个参数:arg以及verbose。随后通过被修饰函数的register函数作为带参装饰器,为不同的数据类型定义不同的函数实现。最后虽然在调用这个相同的fun函数,但是随着传递的参数不同,获得的结果也不相同。

    上述代码的执行结果如下所示:

    False
    Hello, World!
    Let me just say, test
    Strength in numbers, eh? 42
    Enumerate this: 
    0 spam
    1 spam
    2 eggs
    3 spam
    Nothing.
    0.615
    

    如果对于某个具体的数据类型,没有注册其对应的函数实现,此时通常会寻找一个更通用的实现。由于最初的函数是使用@singledispatch作为装饰器构建的,而其默认的实现是object类,所以,如果某个数据类型的操作没有找到具体的实现,那么就会寻找object类中的通用实现。

    要查看给定数据类型对应的具体函数实现,可以使用dispatch方法。具体如下所示:

    fun.dispatch(float)
    

    其输出结果如下所示:

    <function __main__.fun_num(arg, verbose=False)>
    

    上述输出显式实现的函数为fun_num。

    而对于未曾定义具体重载函数的dict数据类型,则会调用默认的fun函数,具体如下所示:

    fun.dispatch(dict)
    

    其输出结果如下所示:

    <function __main__.fun(arg, verbose=False)>
    

    如果要查看重载函数中所有已经实现的数据类型,可以调用重载函数的registry进行查看。具体如下所示:

    fun.registry.keys()
    

    上述的输出结果如下所示:

    dict_keys([<class 'object'>, <class 'int'>, <class 'list'>, <class 'NoneType'>, <class 'decimal.Decimal'>, <class 'float'>])
    

    查看某个具体数据类型对应的函数实现,可以执行如下操作:

    fun.registry[float]
    

    列出的结果与调用重载函数的dispatch方法类似,具体如下所示:

    <function __main__.fun_num(arg, verbose=False)>
    
  7. functools.total_ordering:这是一个类装饰器,接收类或者类的实列对象作为参数。被装饰的类中定义一个或者多个顺序比较方法,剩下的比较方法由装饰器实现。其基本使用形式如下:

    @functools.total_ordering
    

    这个装饰器的使用示例如下所示:

    由于是类装饰器,所以需要定义一个类,并对这个类应用这个装饰器。具体如下所示:

    from functools import total_ordering
    
    
    @total_ordering
    class Student:
     def __init__(self, l_name, f_name):
         self.lastname = l_name
         self.firstname = f_name
    
     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()))
    
    bob = Student('Bob', 'Kenedy')
    alice = Student('Alice', 'Bush')
    
    print('{} is greater than {}? \n{}'.format((bob.lastname + ' ' + bob.firstname),
                                            (alice.lastname + ' ' + alice.firstname),
                                            (bob > alice)))
    

    上述代码的输出结果如下所示:

    Bob Kenedy is greater than Alice Bush? 
    True
    

    在上面的Student类中并没有定义__gt__这个方法,但是在执行对象比较的时候,是可以进行大于关系比较的。

    不过我在PyCharm中,注释掉@total_ordering这一样,也并不影响程序的正确执行。

  8. functools.update_wrapper:该函数也是一个高阶函数,其基本形式如下所示:

    functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=
    WRAPPER_UPDATES)
    

    该函数的作用是更新wrapper函数,使其看起来更像wrapped函数。除了这两个参数之外,还有两个可选参数:assigned以及updated,他们的默认值分别为assigned=WRAPPER_ASSIGNMENTS以及updated=WRAPPER_UPDATES。其中前者指定了使用初始函数的哪些属性值可以直接分配给wrapper函数的对应属性;后者指定了wrapper函数的哪些属性可以使用初始函数中的相关属性值进行更新。其中WRAPPER_ASSIGNMENTS中指定的属性值可以赋值给wrapper函数中的__module__, __name__, __qualname__, __qualname__以及__doc__这几个属性;而WRAPPER_UPDATES中指定的属性值则可以用于更新wrapper函数的__dict__属性,即实例字典。

    为了允许访问初始函数,这个函数会自动向wrapper函数中添加__wrapped__属性用于引用被封装的函数。

    将该函数作为函数装饰器使用的主要目的,就是封装被装饰的函数,并且返回装饰函数,即wrapper函数。如果wrapper函数没有被更新,那么被返回函数的元数据将会直接反射wrapper函数定义,而不是初始函数定义。

    该函数的使用示例如下:

    示例代码如下:

    import functools as ft
    
    
    # Defining the decorator
    def hi(func):
    
     def wrapper():
         "Hi has taken over Hello Documentation"
         print("Hi geeks")
         func()
    
     # Note The following Steps Clearly
     print("UPDATED WRAPPER DATA")
     print(f'WRAPPER ASSIGNMENTS : {ft.WRAPPER_ASSIGNMENTS}')
     print(f'UPDATES : {ft.WRAPPER_UPDATES}')
    
     # Updating Metadata of wrapper 
     # using update_wrapper
     ft.update_wrapper(wrapper, func)
     return wrapper
    
    @hi
    def hello():
     "this is the documentation of Hello Function"
     print("Hey Geeks")
    
    print(hello.__name__)
    print(hello.__doc__)
    help(hello)
    

    上述代码的输出结果如下所示:

    UPDATED WRAPPER DATA
    WRAPPER ASSIGNMENTS : ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__')
    UPDATES : ('__dict__',)
    hello
    this is the documentation of Hello Function
    Help on function hello in module __main__:
    
    hello()
     this is the documentation of Hello Function
    

    如果注释掉上述代码的第19行,即不再执行属性更新,再次执行的结果如下所示:

    UPDATED WRAPPER DATA
    WRAPPER ASSIGNMENTS : ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__')
    UPDATES : ('__dict__',)
    wrapper
    Hi has taken over Hello Documentation
    Help on function wrapper in module __main__:
    
    wrapper()
     Hi has taken over Hello Documentation
    
    

    从上述输出中可以看出,此时的函数属性为装饰器的内嵌函数的信息,而不是被装饰函数的信息。而装饰器的内嵌函数信息恰恰是需要隐藏的。

  9. functools.wraps:该函数的使用形式如下所示:

    @functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
    

    这个函数是functools.update_wrapper函数的进化版,使用更方便,当定义封装函数的时候,可以作为装饰器来使用。其效果等效于偏函数functools.partial的如下形式调用:

    partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)
    

    该函数的使用示例如下:

    示例代码如下:

    # functools.wraps装饰器的使用示例
    
    from functools import wraps
    
    
    def my_decorator(f):
     @wraps(f)
     def wrapper(*args, **kwargs):
         print('Calling decorated function')
         return f(*args, **kwargs)
     return wrapper
    
    
    @my_decorator
    def example():
     """
     Docstring
     """
     print('Called example function')
    
    example()
    print('-'*10)
    print(example.__name__)
    print('#'*10)
    str(example.__doc__)
    

    上述代码的输出结果如下所示:

    Calling decorated function
    Called example function
    ----------
    example
    ##########
    
    '\n    Docstring\n    '
    

    此时返回的信息就是被装饰函数的信息,而不是装饰器的内嵌函数的信息。达到了属性传递的目的。

    如果注销掉代码部分的第7行,输出结果如下所示:

    Calling decorated function
    Called example function
    ----------
    wrapper
    ##########
    
    'None'
    

    此时由于没有进行属性传递,所以返回的就是装饰器的内嵌函数的信息,这并不是我们想要的。

3. 用Python实现自己的functools.lru_cache

functools.lru_cache这个函数利用内存作为缓存,将曾经计算过的结果缓存下来,并在缓存周期内的重复调用时,直接从内存返回结果,而无需重复计算。通过缓存,达到节省计算时间开销的目的,从而提升计算效率。

functools.py这个源码文件中,记录了这个函数的具体实现。该方法的核心是利用字典进行缓存数据的存储,将函数调用时的参数名以及参数值构成的可哈希对象作为字典的键,而将计算结果作为该键的值存储在字典中。当每次调用相同的操作时,会首先查找字典中的键,查看本次调用的参数名以及参数值是否与字典中的键一致,如果一致,则直接从字典中提取值返回该该函数;否则调用该函数求取当次调用的结果,并将函数的参数名和参数值作为键,当次计算结果为值存储在字典中。

下面的简单实现中,并未考虑缓存字典的大小限制,所以也就未对字典中很早之前的计算结果清除出字典,从而使新的计算结果能被加入到缓存字典中(而这一步,就是这个缓存被命名为lru的原因,即 Least Recently Used——最近最少被使用到的)。所以这个初版只是实现了简单的缓存功能,且并未对缓存大小进行限制。具体实现如下所示:

实现的代码如下:

from functools import wraps
import datetime
import time


#########################################################################
### cache codes
#########################################################################
def make_key(*args, **kwargs):
 m_key = ()
 m_key += args
 if kwargs:
     for item in kwargs.items():
         m_key += item
 return m_key


def my_lru_cache(func):
 cache = {}

 @wraps(func)
 def wrapper(*args, **kwargs):
     d_key = make_key(*args, **kwargs)
     if d_key in cache:
         return cache[d_key]
     else:
         res = func(*args, **kwargs)
         cache[d_key] = res
         return res

 return wrapper


#########################################################################
### test codes
#########################################################################
@my_lru_cache
def fib(num: int) -> int:
 if num < 2:
     return num
 else:
     return fib(num - 1) + fib(num - 2)


@my_lru_cache
def add(a, b):
 time.sleep(1)	# sleep 1秒,以便查看缓存效果
 return a + b


def test(num: int) -> int:
 start = datetime.datetime.now()
 fib_res = fib(num)
 delta = (datetime.datetime.now() - start).total_seconds()

 print('the {}\'s fibonacci result is {}, \ncalculate in {} seconds'.format(num, fib_res, delta))
 print()
 print('*' * 15)
 start1 = datetime.datetime.now()
 add_res1 = add(4, b=6)
 add_res2 = add(a=4, b=6)
 add_res3 = add(6, b=4)
 add_res4 = add(b=6, a=4)
 add_res5 = add(a=4, b=6)
 delta1 = (datetime.datetime.now() - start1).total_seconds()
 print(add_res1)
 print(add_res2)
 print(add_res3)
 print(add_res4)
 print('five call of the add method takes {} seconds'.format(delta1))


n = 101
test(n)

上述代码的输出结果如下所示:

the 101's fibonacci result is 573147844013817084101, 
calculate in 0.000999 seconds

***************
10
10
10
10
five call of the add method takes 4.02293 seconds

Process finished with exit code 0

从上述结果中可以看出,计算斐波那契数列的第101项所需要的时间很短,基本瞬间就完成了。

而在5次add方法调用中,如果不适用缓存,应该总的计算时间是5秒,但是由于有2个函数采用了相同的调用参数,相当于这两次调用中的第二次调用是直接使用了缓存结果,而并未真正调用add函数。所以最终总的调用时间为4秒多一点。

4. 类型注解及其背后的目的

Python是动态语言,并不同于C、C++之类的编译型静态语言那般,在程序编译阶段就确定好了数据类型;Python只有在程序执行的时候,才能知道传递的数据类型是否符合程序逻辑,如果不符合,则抛出异常。由于没有强制的类型检查,这也就导致Python程序只有在执行的时候才能发现一些数据类型上带来的潜在问题。

另外一点,就是由于函数定义的时候并不像C、C++那样指定变量类型以及返回值类型,所以没有函数的注释信息以及相关文档,则很难直接看出来函数设计者的意图。这就导致新的API往往难以使用的问题。

为了解决上述问题,Python引入了文档字符串(比如在函数或者类定义的语句体的开头部分使用'''docstring'''或者"""docstring"""指明的函数作用、参数类型、期望的输入参数以及函数使用帮助等等信息)。另外一个举措就是类型注释,通过类型注释,提示使用者函数参数的期望类型、函数的返回值类型等信息。需要注意的是,类型注释并不是强制性的,即便定义了类型注释,Python也不会做强制的类型检查和约束,只是起到提示以及易于阅读的目的。

关于Docstring的示例,如下所示:

示例代码如下所示:

def add(x, y):
 """
 calculate the sum of two numbers

 :param x: int, float, decimal.Decimal
 :param y: int, float, decimal.Decimal
 :return: int, float, decimal.Decimal
 """
 return x + y

help(add)

上述的函数体的最开始部分,由三个连续的单引号或者双引号,构建起来的内容,就是docstring,其中第一行为该函数的简单说明;空一行,然后是函数的参数类型以及返回值类型注释信息。

在注释信息之后,是函数语句体。

docstring部分的内容会出现在help函数调用的输出结果中。上述代码的执行结果如下所示:

Help on function add in module __main__:

add(x, y)
 calculate the sum of two numbers

 :param x: int, float, decimal.Decimal
 :param y: int, float, decimal.Decimal
 :return: int, float, decimal.Decimal

上述即为docstring的作用以及使用方式。

关于类型注释的示例,如下所示:

示例代码如下所示:

import inspect


def add_anno(x:int, y:int=5) -> int:
 return x + y

print(add_anno(3))		# 8
print(add_anno(3, 9))	# 12

insp = inspect.signature(add_anno)
print(insp)				# (x: int, y: int = 5) -> int
print(insp.parameters)	# OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int = 5">)])
print(insp.return_annotation)		# <class 'int'>

上述的函数定义头部分的参数列表中,通过在函数名后面加上冒号以及数据类型,标识该参数的期望数据类型。如果此时要对参数指定默认值,只需要将等号以及值写在冒号数据类型后面即可,如上述代码的第4行所示。

上述代码的输出结果如下所示:

8
12
(x: int, y: int = 5) -> int
OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int = 5">)])
<class 'int'>

上述即为类型注释的使用方式。

上述的两种使用形式可以结合在一行,并不是彼此对立的。

5. References

[1]. Are these attribute bindings necessary in the implementation of functools.partial?

[2]. PEP 443 – Single-dispatch generic functions

[3]. Python Functools – update_wrapper()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值