Python_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())

# 输出
11
11

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

  • ounter 函数每调用一次生成的都是一个新的函数对象
  • foo1 和 foo2 是相互独立的两个函数
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

# 输出
False

is 的比较规则是:先比较元素的内存地址,然后比较元素的内容。首先 foo1 和 foo2 属于不同的对象,所以内存地址肯定不同,函数对象无需进行函数内容的比较,所以这里返回 False

1.1. 自定义 sort 函数

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

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]))

# 输出
[1, 2, 3, 5, 6, 7, 9]

分析:

  • 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 flag:
                    new_list.insert(i, value)
                    break
        else:
            new_list.append(value)
    return new_list

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

# 输出
[9, 7, 6, 5, 3, 3, 2, 1]

分析:

  • 倒序有两种表达方式,即正序排好,然后倒着截取。或者是按照倒序排列,这里使用倒序排列。
  • 正序时:如果 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

print(sort(['a',1,2,'b'], key=str, reverse=True))

# 输出
['b', 'a', 2, 1]

分析:

  • 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))

# 输出
['b', 'a', 2, 1]
  • 传参时利用了 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 : lst
Out: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]

In : import random
In : random.shuffle(lst)

In : lst
Out: [70, 45, 90, 40, 30, 80, 25, 55, 5, 75, 85, 95, 50, 20, 35, 15, 10, 60, 65, 0]

In : sorted(lst, reverse=True)
Out: [95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 0]

In : lst.append('a')

In : 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 : sorted(lst, key=lambda x:str(x))
Out: [0, 10, 15, 20, 25, 30, 35, 40, 45, 5, 50, 55, 60, 65, 70 75, 80, 85, 90, 95, 'a']

In : lst.remove('a')

In : sorted(lst,key=lambda x : 100 - x)
Out: [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 : lst1 = [1,9,55,150,-3,78,28,123]

In : list(filter(lambda x:x%3==0, lst1))
Out: [9, 150, -3, 78, 123]

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

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

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

1.2.3. map 映射

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

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

  • func:一个函数,用于处理 iterable 的元素,在指定多个 iterable 对象时,函数的参数个数iterable 对象的个数是相等的。
  • *iterable:一个或多个可迭代对象

In : list(map(str,range(10)))
Out: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

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

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

In : dict(map(lambda x:(x%5,x), range(500)))
Out: {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)
        duration = (datetime.datetime.now() - start).total_seconds()
        print(' 函数:{} 执行用时:{}'.format(wrapper.__name__, duration))
        return res
    return wrapper

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

# 执行结果:

In : 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(args)

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 被调用而已!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值