13 - 高阶函数-柯里化-装饰器

1 高阶函数

        在Python中一切皆对象,固然也包括函数。函数在Python中是一等公民(First Class Object)。即函数与字符串数组整型无异,它能够被命名能够被赋值能够看成参数被传进另外一个函数也能够被另外一个函数看成返回值能够放在任何位置,简单来讲:程序员

  • 函数也是一个对象,而且是一个可调用对象(callable)
  • 函数能够做为普通变量、参数、返回值等等

        那什么是高阶函数呢?在Python中咱们能够理解为:当一个函数接受另外一个函数做为参数使用,或者一个函数最后返回了另一个函数。在这两种状况下,这个函数就能够称之为高阶函数(知足以上任意一种状况便可,没必要同时知足)。算法

数学概念: y = g(f(x))数组

def outer(x):
    def inner(step=1):
        nonlocal x    # 声明局部变量x不是一个局部变量,应该在外层寻找 
        x += step
        return x
    return inner
foo1 = outer(10)
foo2 = outer(10)
print(foo1())
print(foo2())

        内层函数inner还引用了外层函数的自由变量x,造成了闭包,因为外部函数返回了内部函数,因此这就是一个典型的高阶函数。针对上面实例还须要说明是:闭包

  • ounter函数每调用一次生成的都是一个新False的函数对象
  • foo1和foo2也都是相互独立的两个函数
In [1]: def outer(x):
   ...:     def inner(step=1):
   ...:         nonlocal x    # 声明局部变量x不是一个局部变量,应该在外层寻找
   ...:         x += step
   ...:         return x
   ...:     return inner
   ...: foo1 = outer(10)
   ...: foo2 = outer(10)
   ...: foo1 is foo2

is的比较作则是:先比较元素的内存地址,而后比较元素的内容。首先 foo1和foo2 属于不一样的对象,因此内存地址确定不一样,而后因为函数对象没有实现函数内容的比较因此这里返回Falseapp

1.1 自定义sort函数

        从头构建一个相似于内置函数sorted的函数,来体会高阶函数的做用,顺便理解一下sorted的原理。dom

1.1.1 将规模缩小,先实现排序,先无论key和reverse参数

def sort(iterable, *, key=None, reverse=False):
    new_list = []
    for value in iterable:
        for i, k in enumerate(new_list):
            if value < k:
                new_list.insert(i, value)
                break
        else:
            new_list.append(value)
    return new_list

print(sort([1,6,2,7,9,3,5]))

分析:函数

  • sort返回一个新的列表,咱们这里构建一个列表,用于存放排序好的列表
  • 为了练习算法,这里选择使用直接插入排序,注意:直接插入排序是原地排序,这里是变化,直接插入到新的已排序列表中去
  • 利用enumerate构建索引,用于确认插入位置,注意:由于new_list在咱们这个场景下是有序的!因此才能够这样用
  • 初始状况下,因为new_list为空,因此待排序列表的第一个元素会被直接插入到已排序列表的首位
  • 后续只需在待排序的列表中,拿出一个数据,和已排序区的元素,从左至右依次对比便可
  • 若是大于已排序区的全部元素,那么直接追加便可,若是小于某一个元素只须要在对应的元素为插入待排序元素便可
  • 若是小于的话,由于列表的特性,在一个位置上插入数据,那么原数据会自动向右移动,因此符合直接插入排序的原理

1.1.2 添加reverse参数判断

def sort(iterable, *, key=None, reverse=False):
    new_list = []
    for value in iterable:
        for i, k in enumerate(new_list):
            flag = value > k if reverse else value < k
            # if reverse:
            #     flag = value > k
            # else:
            #     flag = value < k
            if flag:
                    new_list.insert(i, value)
                    break
        else:
            new_list.append(value)
    return new_list
    # return new_list[::-1]
print(sort([1,6,2,7,9,3,3,5]))

分析:学习

  • 倒序有两种表达方式,即正序排好,而后倒着截取。或者是按照倒序排列,这里使用倒序排列。
  • 正序时:若是value大于k,那么须要把value插入到k的位置上
  • 倒序时,若是value小于k,那么须要把value插入到k的位置上
  • 因此添加flag来采集用户传入的reverse参数,这里使用了三元表达式来简化代码

1.1.3 添加key参数判断

def sort(iterable, *, key=None, reverse=False):
    new_list = []
    for value in iterable:
        value_new = key(value) if key else value
        for i, k in enumerate(new_list):
            k_new = key(k) if key else k
            flag = value_new > k_new if reverse else value_new < k_new
            if flag:
                    new_list.insert(i, value)
                    break
        else:
            new_list.append(value)
    return new_list
    # return new_list[::-1]
print(sort(['a',1,2,'b'], key=str, reverse=True))

分析:优化

  • key传入了一个函数,用于对每一个key进行转换,而后使用转换后的元素来进行比较,因此咱们这里使用了转化后的变量来比较
  • 当key能够被调用,那么咱们认为它是一个函数,那么调用他对元素进行转换。
  • 这里传入了str函数,若是结合前面所学的知识能够改成lambda表达式。
def sort(iterable, *, key=None, reverse=False):
    new_list = []
    for value in iterable:
        value_new = key(value) if callable(key) else value
        for i, k in enumerate(new_list):
            k_new = key(k) if callable(key) else k
            flag = value_new > k_new if reverse else value_new < k_new
            if flag:
                    new_list.insert(i, value)
                    break
        else:
            new_list.append(value)
    return new_list
print(sort(['a',1,2,'b'], key=lambda x:str(x), reverse=True))
  • 传参时利用了lambda表达式,在函数内部,每次传入一个参数,返回它的str对象,其实效果等同于str,这里只是提一下lambda表达式。

1.2 内建函数(高阶函数)

Python内置了不少高阶函数的应用,这里仅介绍较为经常使用的。

  • sorted: 排序函数,直接返回新的列表
  • filter:过滤函数,返回一个迭代器
  • map:映射函数,返回一个迭代器
  • zip:拉链函数,返回一个迭代器

1.2.1 sorted排序

sorted(iterable, /, *, key=None, reverse=False)

当即返回一个新的列表,对一个可迭代对象的全部元素排序。

  • key: 排序规则为key定义的函数
  • reverse: 表示是否进行翻转排序
In [49]: lst
Out[49]: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
In [50]: import random
In [51]: random.shuffle(lst)
In [52]: lst
Out[52]: [70, 45, 90, 40, 30, 80, 25, 55, 5, 75, 85, 95, 50, 20, 35, 15, 10, 60, 65, 0]
In [53]: sorted(lst,reverse=True)
Out[53]: [95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 0]
In [54]: lst.append('a')
In [55]: sorted(lst,key=str,reverse=True)
Out[55] :['a', 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 5, 45, 40, 35, 30, 25, 20, 15, 10, 0]
In [58]: sorted(lst,key=lambda x:str(x))
Out[58]: [0, 10, 15, 20, 25, 30, 35, 40, 45, 5, 50, 55, 60, 65, 70 75, 80, 85, 90, 95, 'a']
In [59]: lst.remove('a')
In [61]: sorted(lst,key=lambda x : 100 - x )
Out[61]: [95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 0]
  • 这里定义了一个接受一个参数的匿名函数,而后返回处理后的元素(这是匿名函数最经常使用的场景之一)
  • 功能等同于直接调用str函数

    sorted直接返回一个新的列表,而列表也有sort方法,经过list.sort()进行排序是直接原地进行排序的。

1.2.2 filter 过滤

filter(function or None, iterable) --> filter object

过滤可迭代对象的元素,返回一个迭代器

  • function:表示一个函数,它的功能是:每次从可迭代对象iterable取出一个元素交给function函数处理,若是返回True,则保留该元素,不然提出该元素,若是是None,表示剔除等效False的对象
  • iterable:可迭代对象
In [62]: lst1 = [1,9,55,150,-3,78,28,123]
In [64]: list(filter(lambda x:x%3==0,lst1))
Out[64]: [9, 150, -3, 78, 123]


# 等同于
In [66]: def func(iterable):
    ...:     for i in iterable:
    ...:         if i % 3 == 0:
    ...:             yield i
    ...:

In [67]: list(func(lst1))
Out[67]: [9, 150, -3, 78, 123]


# 或者
In [66]: def func(iterable):
    ...:     for i in iterable:
    ...:         if (lambda x:x%3==0)(i):    ## l这里属于函数调用:func()()
    ...:             yield i
    ...:

1.2.3 map 映射

map(func, *iterables) --> map object

对多个可迭代对象的元素按照指定的函数进行映射,返回一个迭代器

  • func:一个函数,用于处理iterable的元素,在指定多个iterable对象是,函数的参数数量与iterable的数量是相等的。
  • *iterable:一个或多个可迭代对象
In [69]: list(map(str,range(10)))
Out[69]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

In [70]: list(map(lambda x:x+1,range(10)))
Out[70]: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

In [73]: dict(list(map(lambda x,y:(x,y),'abcde',range(10))))
Out[73]: {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4}

In [74]: dict(map(lambda x:(x%5,x),range(500)))
Out[74]: {0: 495, 1: 496, 2: 497, 3: 498, 4: 499}

map对象的长度,等同于最小的iterable长度。(木桶效应)

2 柯里化

        在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数并且返回结果的新函数的技术。-- 来自维基百科的解释
        总结一下:柯里化指的就是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。其数学表达式为:

z = f(x, y) 转换成 z = f(x)(y) 的形式

这里把经典的add函数拿来进行转换:

  • 想要转换为add(4)(5),那么add(4)必需要返回一个函数,不然没法将5看成参数传入
def add(x, y):
    return x + y

def new_add(x):
    def inner(y):
        return x + y

    return inner


print(add(4, 5))     # 9
print(new_add(4)(5)) # 9
print(add(4, 5) == new_add(4)(5))  # True

经过函数嵌套,就能够完成函数的柯里化了,对柯里化有所了解之后,那么咱们就能够继续来看Python中对咱们小白来讲的第一个难点:装饰器

3 装饰器

        什么是装饰器?归纳的讲,装饰器的做用就是为已经存在的对象添加额外的功能。咱们接下来从一个需求开始学习装饰器。

3.1 需求分析

如今有以下函数,咱们须要将这个函数的日志打印到终端

def add(x,y):
    print('我被调用执行啦')  # 新增打印日志语句
    return x + y
add(100,200)

        可是仔细思考,打印日志是一个独立的功能,它和add函数自己并无什么关联关系,咱们说一个函数是为完成一个工程的,因此直接写在函数里面不是不能够,可是不建议,而且打印日志属于调试信息功能,与业务无关,不该该放在业务函数加法中。
        若是不须要写在函数的里面,那么咱们得想办法写在函数的外面

def add(x, y):
    return x + y

def logger(fn, x, y):
    print('函数开始执行')
    res = fn(x, y)
    print('函数执行完毕')
    return res

logger(add,4,5)
  • 这样就解决了打印语句在函数内定义的问题了。
  • 可是若是add函数的形参不少,咱们要挨个写上吗?因此*args,**kwargs帮了咱们很大的忙
def add(x, y):
    return x + y

def logger(fn, *args, **kwargs):
    print('函数开始执行')
    res = fn(*args, **kwargs)
    print('函数执行完毕')
    return res

logger(add,4,5)

3.2 函数柯里化

        使用logger(add,4,5)来调用咱们的add函数真是太丑了,给其余人看,可能人家也不知道你要干啥,结合前面所学的柯里化,咱们进行以下变更

def add(x, y):
    return x + y

def logger(fn):
    def wrapper(*args, **kwargs):
        print('函数被执行了')
        res = fn(*args, **kwargs)
        print('函数执行完毕')
        return res
    return wrapper

logger(add)(4,5)

        这样看起来是否是就好看多了?当指定logger(add)(4,5)时,才会打印日志,但若是想要在全部调用add函数的地方,咱们还须要在全部调用add的地方修改成logger(add)(参数),想想,若是我能把logger(add)变成add是否是就能够直接写成add(4,5)了呢?

logger(add)(4,5)
--------
add = logger(add)
add(4,5)    # 将add从新指向了新的函数wrapper

        按照柯里化的原型中logger(add)返回了一个函数wrapper,而咱们的(4,5)实际上是传递给了wrapper,结合咱们前面所学的高阶函数,这里的wrapper,是一个闭包函数,由于在内部对fn进行了执行,并且增长了打印日志的功能,咱们在执行wrapper的同时,也会执行原来的函数fn,而且添加了打印日志的功能,因此logger就是一个装饰器函数!!!

3.3 装饰器函数(语法糖)

        语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·蘭丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,可是更方便程序员使用。 语法糖让程序更加简洁,有更高的可读性。Python针对咱们刚刚编写的logger(add)函数,进行了语法糖优化,因此下面是咱们使用语法糖以后的

def logger(fn):
    def wrapper(*args, **kwargs):
        print('函数被执行了')
        res = fn(*args, **kwargs)
        print('函数执行完毕')
        return res
    return wrapper

@logger   # 等于 add = logger(add)
def add(x, y):
    return x + y

add(4,5)

        当解释器执行到@logger时,会自动把它下面的函数看成参数,传给logger函数,因此这里@logger 其实就等于 add = logger(add) 另外,logger必需要定义在add函数以前才能够被装载!这一点很重要!

3.4 装饰器带来的问题

利用装饰器计算以下函数的运行时间

import time
import datetime

def logger(fn):
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)
        total_seconds = (datetime.datetime.now() - start).total_seconds()
        print('函数:{} 执行用时:{}'.format(wrapper.__name__,total_seconds))
        return res
    return wrapper

@logger
def add(x, y):
    time.sleep(2)
    return x + y

执行结果:
In [76]: add(4,5)
函数:wrapper 执行用时:2.000944

这里__name__表示函数的名称.

        什么鬼?这里为何打印的是wrapper啊,为何不是add呢?这样的话,别人不就发现我把这个函数给偷偷换掉了吗?不行不行,我得想个办法把函数的属性复制过来,因为这个功能和打印用时的装饰器不是一个功能,那么咱们还得给装饰器另加一个装饰器。-_-!

import time
import datetime

def copy_properties(old_fn):
    def wrapper(new_fn):
        new_fn.__name__ = old_fn.__name__
        return new_fn
    return wrapper

def logger(fn):
    @copy_properties(fn)  # wrapper = copy_properties(fn)(wrapper)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)
        total_seconds = (datetime.datetime.now() - start).total_seconds()
        print('函数:{} 执行用时:{}'.format(wrapper.__name__,total_seconds))
        return res
    return wrapper

@logger
def add(x, y):
    time.sleep(2)
    return x + y

add(4,5)
  • 解释器执行到 @copy_properties(fn) 时,会把下面的wraper装入,等于wrapper = copy_properties(fn)(wrapper)
  • 因为知道了参数的个数(必定是一个函数对象),这里就没有使用*args, **kwargs
  • 函数的属性不止__name__一个,其余的怎么办呢?

3.5 拷贝函数属性

        Python的内置模块functools中,内置了不少经常使用的高阶函数,其中wraps就是用来拷贝函数的属性及签名信息的。利用wraps,咱们就不须要本身编写copy_properties函数了,下面是修改后的版本

import time
import datetime
import functools

def logger(fn):
    @functools.wraps(fn)  # wrapper = functools.wraps(fn)(wrapper)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)
        total_seconds = (datetime.datetime.now() - start).total_seconds()
        print('函数:{} 执行用时:{}'.format(wrapper.__name__,total_seconds))
        return res
    return wrapper

@logger
def add(x, y):
    time.sleep(2)
    return x + y

add(4,5)

经过使用 @functools.wraps(fn) 咱们能够方便的拷贝函数的属性签名信息,好比:'module', 'name', 'qualname', 'doc','annotations'等,这些属性信息,将在后续部分进行讲解,这里知道便可

4 代参装饰器

        上面章节讲到的是带一个参数(函数)的装饰器,在Python中这种装饰器被称为无参装饰器,由于语法糖的表现形式就是 @logger,下面要说的是代参数的装饰器,即@logger(50)

4.1 仍是从一个需求开始

        以上述函数为例,咱们须要记录当函数执行超过必定时间时的日志信息,该怎么办呢?假设这个时间是5秒,那么很显然,咱们须要把这个时间变量传入到装饰器中进行判断。也就是说咱们须要写成这种形式:

logger(5)(add)

looger(5)返回的是一个函数,不然没法将add传入

4.2 代参装饰器编写

import time
import datetime
import functools

def logger(var):
    def inner(func):
        def wrapper(*args, **kwargs):
            start = datetime.datetime.now()
            res = func(*args, **kwargs)
            total_seconds = (datetime.datetime.now() - start).total_seconds()
            if total_seconds > var:
                print('函数执行时间过长')
            return res
        return wrapper
    return inner

@logger(5)  # logger(5)(add)
def add(x, y):
    time.sleep(6)
    return x + y

是否是很简单? 在掌握了柯里化以及无参装饰器。

4.3 代参装饰器小结

代参装饰器有以下特色:

  1. 它是一个函数
  2. 函数做为它的形参
  3. 返回值是一个不带参数的装饰器函数
  4. 使用@function_name(参数列表)方式调用
  5. 能够看作在装饰器外层又加了一层函数

    装饰器何时被执行?,还记得@logger等于什么吗? add = logger(add) 等号等于赋值,是否是要先计算右边的?因此,装饰器在函数定义阶段就已经被执行了!不是等到被装饰的函数执行时,才执行哦!真正执行时,每次都会生成一个新的wrapper被调用而已!

相关文章
相关标签/搜索