Python函数之高阶函数&递归&匿名函数

一 高阶函数

变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另外一个函数作为参数,这种函数就称之为高阶函数。

def my_add(x,y,func):
    return func(x) + func(y)

res = my_add(-11,5,abs)
print(res)  # 16 

只需要满足以下任意条件,即是高阶函数:

  • 接收一个或多个函数作为输入
  • return 返回另外一个函数

二 递归

2.1 什么是递归

我们知道,在函数内部,可以调用其他函数。如果一个函数在内部调用自己本身,这个函数就是递归函数。举个例子:采用递归的方式计算1~n的和:

def my_sum(n):
    if n == 1:
        return 1
    return my_sum(n - 1) + n

print(my_sum(5))       # 15

实际的执行步骤我们可以拆解如下:

# my_sum(5) = my_sum(4) + 5 = my_sum(3) + 4 + 5 = my_sum(2) +3 + 4 + 5 = my_sum(1) + 2 + 3+ 4 + 5 = 1 + 2 + 3+ 4 + 5 = 15

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

2.2 最大层数限制

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多(超过最大层数限制),会导致栈溢出。我们可以试试my_sum(1000):

在执行上面的示例时,会报错:

RecursionError: maximum recursion depth exceeded in comparison

如何修改最大层数限制?

import sys
a = sys.getrecursionlimit()
sys.setrecursionlimit(2000)   # 修改最大层数限制

def func(n):
    print(n)
    func(n+1)

func(1)

虽然可以进行修改最大层数限制,但是因为不是尾递归,仍然要保存栈,内存大小一定,不可能无限递归,而且无限制地递归调用本身是毫无意义的。

2.3 尾递归

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。

尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

上面的my_sum(n)函数由于return my_sum(n - 1) + n引入了加法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的和传入到递归函数中:

def my_sum(n):
    return my_sum_iter(n, 0)

def my_sum_iter(num, sum):
    if num == 0:
        return sum
    return  my_sum_iter(num - 1, num + sum)

my_sum(5)对应的my_sum_iter(5, 0)的调用如下:

# my_sum_iter(5, 0)
# my_sum_iter(4, 5)
# my_sum_iter(3, 9)
# my_sum_iter(2, 12)
# my_sum_iter(1, 14)
# my_sum_iter(0, 15)
# 15

尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。但大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的my_sum(n)函数改成尾递归方式,也会导致栈溢出。

2.4 递归调用的两个阶段:递推&回溯

递归应该分为两个明确的阶段:回溯与递推

回溯:从外向里一层一层递归调用下去, 回溯阶段必须要有一个明确地结束条件,每进入下一次递归时,问题的规模都应该有所减少(否则,单纯地重复调用自身是毫无意义的)

递推:从里向外一层一层结束递归

2.5 实例应用

汉诺塔问题的实现

二分法的实现

2.6 总结

综上,我们在使用递归时,需要注意以下几点:

1. 必须有一个明确的结束条件

2. 每次进入更深一层递归时,问题规模相比上次递归都应有所减少

3. Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题

三 匿名函数

3.1 概述

有些时候,当我们在传入函数时,不需要显式地定义函数,这时我们可以定义匿名函数。下面我们以map()函数为例展示匿名函数的使用,计算给定数字列表各元素的平方,组成新的列表:

L = [1, 2, 3, 4, 5, 6, 7, 8, 9]
new_L = list(map(lambda x:x*x,L))
print(new_L)        # [1, 4, 9, 16, 25, 36, 49, 64, 81]

通过对比可以看出,匿名函数lambda x:x*x实际上就是:

def func(x):
    return x * x

图解匿名函数:

image

上面的例子效果等价于:

L = [1, 2, 3, 4, 5, 6, 7, 8, 9]

def func(x):
    return x*x

new_L = []
for i in L:
    new_L.append(func(i))

print(new_L)        # [1, 4, 9, 16, 25, 36, 49, 64, 81]

当然,上面的例子我们用列表生成式更简单,这里只是为了说明匿名函数的使用。

用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,我们可以把匿名函数赋值给一个变量,再利用变量来调用该函数:

func = lambda x:x*x
print(func)         # <function <lambda> at 0x000001AC31A2F048>
print(func(10))     # 100

但这样就失去了匿名函数的意义了

同时,我们可以将匿名函数作为返回值返回,例如:

def func():
    return lambda x:x*x

print(func)         # <function func at 0x000002ADB84AF048>
print(func())       # <function func.<locals>.<lambda> at 0x000002ADB84AF1E0>
print(func()(10))   # 100

匿名函数与有名函数的区别:

有名函数:循环使用,保存了名字,通过名字就可以重复引用函数功能

匿名函数:一次性使用,随时随时定义

3.2 实例应用

scores = {
    'zhangsan':87,
    'lisi':92,
    'wangwu':78,
    'xiaoming':99
}

max_score_name = max(scores,key = lambda k:scores[k])
print(max_score_name)

min_score_name = min(scores,key = lambda k:scores[k])
print(min_score_name)
利用max/min获取字典中分数最大最小对应的姓名
posted @ 2018-06-13 00:24  Joe1991  阅读(137)  评论(0)    收藏  举报