Hi,大家好。 我是茶桁。
本节课,我们来学习一下Python中的「高阶函数」。
让我们先来了解一下,什么是递归函数。
递归函数就是定义一个函数,然后在此函数内,自己调用自己。
既然是自己调用自己,那这个函数必须要有一个结束才行,否则会一直重复的调用下去,直到调用层数越来越多,最终会导致栈溢出。
让我们先写一个雏形:
# 初步认识一下递归函数
def recursion(num):
print(num)
recursion(num - 1)
recursion(3)
# 执行结果
3 2 1 0 -1 -2 -3 -4 -5 -6 -7 ....
RecursionError: maximum recursion depth exceeded while calling a Python object
最后,导致栈溢出,程序报错。
那么这个程序到底做了什么?
首先,我们定义了一个函数,然后执行,执行的时候给了一个参数3
。
进入程序之后,先将3
打印了一遍,然后在函数内部,又调用了一遍自己,参数为3-1
,也就是传了一个参数2
,在进入函数之后,打了了2
, 继续自己调用自己,传参2-1
,1-1
, 0-1
, ...
就这样一直循环下去。
那么我们怎么样让这个程序停下来?就是在函数自己调用自己之前,加上一个限制条件:
# 初步认识一下递归函数 3 2 1 0
def recursion(num):
print(num)
# 检测当前值是否到0
if num > 0:
# 调用函数本身
recursion(num - 1)
recursion(3)
# 执行结果
3
2
1
0
我们给调用之前加了一个条件,如果num > 0
才允许继续执行,这样,当程序传递了1-1
之后,执行了最后一次打印,然后就不向下执行了。
不过不要以为程序到这里就结束了,我们多加一行代码试试看:
# 初步认识一下递归函数 3 2 1 0
def recursion(num):
print(num, end=" ")
# 检测当前值是否到0
if num > 0:
# 调用函数本身
recursion(num - 1)
print(num, end=" ") # 又加了一个print函数
recursion(3)
# 执行结果
3 2 1 0 0 1 2 3
如果你不知道程序做了什么,我们稍微分析一下:
解析当前递归函数的执行过程:
``` recursion(3) ==> 3 recursion(3-1) ==> 2 recursion(2-1) ==> 1 recursion(1-1) ==> 0 recursion(0) ==> 0 recursion(1) ==> 1 recursion(2) ==>2 recursion(3) ==> 3 ```
也就是,在递归函数中,程序是一层一层的进入,然后再一层一层的返回。
这就好像是, 我们在上学的时候,你坐在最后一排,但是你有个心仪的女孩坐在最前面。你想要对方电话,这个时候你传递一个纸条给前面的同学,前面的同学再往前传,一直往前传到女孩手里。女孩看完之后,写完回复再一次次的传回来。最后你满怀期待的打开一看:“滚。”
当然,我们的递归函数和这个不同的地方是最后不会多加那个“滚”字。
什么是回调函数呢?
我们首先来思考一个问题:
def func(a):
print(a)
func(a)
在这个简单的函数中,我们已经学会了传值a
给到func()
,那么参数到底可以传一些什么进去?a
可以是什么?能不能是一个函数呢?
这就引出了我们现在的内容:
# 带有回调函数参数的函数
def func(obj):
print(obj, type(obj))
# 并且在函数中调用传递进来的形参函数
obj()
def _self():
print("i am _self")
func(_self)
# 执行结果
<function _self at 0x111ed4280> <class 'function'>
i am _self
可以看到,我们选择执行的是func
函数,但是最后打印出了_self
函数中语句。原因就是我们在执行func
函数的时候,将_self
函数作为参数传递给了func
的形参obj
, 我们在其中打印了obj
以及obj
的类型,并且最后执行了一下obj
, 实际上也就是执行了一遍_self
函数。
如果在一个函数中要求传递的参数是一个函数作为参数,并且在函数中使用了传递进来的函数,那么这个函数我们就可以称为是一个回调函数。
我们拿系统内部的一个现成的函数来重新封装一个新的函数来试试:
# 做一个数学计算的函数
def func(x, y, obj):
"""
此函数用来整合其他的数学运算
在当前函数中,需要接收三个参数,前两个为数值,最后一个为函数
x, y: int
f: function
:return:
"""
print(obj(x, y))
func(2, 3, pow)
# 执行结果
8
在日后使用这个函数的时候,就可以传入数值和要做什么计算的方法,就可以了。
当然,这个函数写的并不完善,比如,我们在执行func(2, 3, sum)
的时候就会报错,原因是因为sum()
函数内部是要进行迭代的的,然而int
类型中没有魔法方法__iter__
, 所以无法迭代。所以,要想这个函数具有通用性,还需要在内部完成很多工作。
之前我们在回调函数中将函数作为参数进行了传递,那么问题来了,既然函数能作为参数进行传递,那能不能作为参数被return
呢?
def person():
money = 0
def work():
print(money)
return work
person()
# 执行结果
<function __main__.person.<locals>.work()>
我们可以看到,work
函数被成功返回出来了。但是并未继续执行, 因为其内部的print()
没起作用。
我们用一个变量来接收这个返回的函数:
def person():
money = 0
def work():
print(money)
return work
res = person()
res()
# 执行结果
0
说明res
接收到返回的work()
函数,并且最后执行成功了。
好了,让我们继续为这个函数做一点什么,看看有什么变化。
def person():
money = 0
def work():
nonlocal money
money += 100
print(money)
return work
res = person()
res()
# 执行结果
100
这个结合前几节所讲的内容就很好理解了对吧? nonlocal
关键字拿到上一层函数定义的变量,然后在内层函数中进行使用,最后打印出来。
那我们继续执行会如何?让我们多执行几次:
# 定义一个函数
def person():
money = 0 # 函数中定义了一个局部变量
# 定义内函数
def work():
nonlocal money # 在内函数中使用了外函数的临时变量
money += 100
print(money)
# 在外函数中返回了内函数,这个内函数就是闭包函数
return work
res = person() # return work res = work
res() # res() == work()
res()
res()
res()
res()
# 此时 就不能够在全局中对money这个局部变量进行任何操作了,
# 闭包的作用:保护了函数中的变量不受外部的影响,但是又能够不影响使用
你会不会认为会一直打印100
? 让我们看看执行结果到底是怎样的:
100
200
300
400
500
怎么样,是不是完全没想到?这个就是闭包函数的特点。
在一个函数内返回了一个内函数,并且这个返回的内函数还使用了外函数中局部变量,这个就是闭包函数。其特点为:
func.__closure__
,如果是闭包函数返回cell
首先,我们先弄清楚什么是匿名函数: 匿名函数的意思就是说可以不使用def
来定义,并且这个函数也没有名字。
在Python中,我们可以使用lambda
表达式来定义匿名函数。我们需要注意,lambda
仅仅是一个表达式,并不是一个代码块,所以lambda
又称为一行代码的函数。
在lambda
表达式中,也有形参,并且不能够访问除了自己的形参之外的任何数据,包括全局变量。其语法如下:
# 语法:
lambda [参数列表]:返回值
让我们来尝试写写看,我们先来定义一个普通的加法运算的函数:
# 封装一个函数做加法运算
# 普通函数
def sum(x, y):
return x+y
print(sum(2, 3))
毫无疑问,执行结果为5
。那么接下来,用lambda
该怎么写呢?
# 改成lambda表达式来封装
res = lambda x, y:x+y
print(res(4, 4))
# 执行结果
8
结合闭包函数的讲解,这里就应该很容易看懂了吧?一样的地方就是,使用了一个变量res
来接收这个返回的函数,然后执行res
函数。
让我们再来一段:
res = lambda sex:"很man" if sex=='male' else '很nice'
print(res('female'))
# 执行结果
很nice
只看结果的话,我们很清楚这段函数最后执行到了else
语句内。但是是如何进入的呢?让我们将这段代码用普通函数的写法展开来看看:
def func(sex):
if sex == 'male':
return '很man'
else:
return '很nice'
func('female')
这样是不是很清晰了?回过头来让我们看刚才那段lambda
表达式,我们可以这样去看:
# lambda 参数列表: 真区间 if 表达式判断 else 假区间
lambda sex: '很man' if sex=='male' else '很nice'
# 然后用一个变量接收函数
res = lambda sex: '很man' if sex=='male' else '很nice'
所以可以看出来,其实lambda
十分的方便,并且并不难理解,当你习惯了lambda
之后,会非常便捷。
迭代器是一个很有意思的功能,可以说是Python中最具特色的功能之一,它是访问集合元素的一种方式。
迭代器是一个可以记住访问遍历的位置的对象。从集合的第一个元素开始访问,直到集合中的所有元素被访问完毕。
迭代器只能是从前往后一个一个的遍历,不能后退。
我们把之前一直使用的range()
拿过来看:
# range(10, 3, -1) 返回一个可迭代的对象
for i in range(10, 3, -1):
print(i, end=" ")
# 执行结果
10 9 8 7 6 5 4
表面上来看,似乎range()
本身就是一个迭代器,可是我们来尝试做个实验:
x = range(5)
print(next(x))
# 执行结果
TypeError: 'range' object is not an iterator
当我们尝试调用next()
函数的时候报错了,被告知range
不是一个迭代器。
那么,range
不是迭代器,究竟是什么呢?这里我们就要先深入研究下迭代器的特性:
严格来说,迭代器是指实现了迭代协议的对象,迭代协议是指实现了iter
方法并返回一个实现了next
方法的迭代器对象,并通过StopIterator
一场标识迭代完成。
iter()
iter()
能把迭代的对象,转为一个迭代器对象,其参数为可迭代的对象(str, list, tuple, dict
), 返回值为迭代器对象。其中需要注意的一点是:迭代器一定是一个可以迭代的对象,但是可迭代的对象并不一定是迭代器。
我们在迭代器上使用iter
会得到相同的对象:
i = iter([1, 2, 3])
print(iter(i) is i)
print(id(iter(i)))
print(id(i))
# 执行结果
True
4425502096
4425502096
基于此,我们可以这样实现:
i = iter([1, 2, 3, 4])
list(zip(i, i))
# 执行结果
[(1, 2), (3, 4)]
next()
next()
函数可以去调用迭代器,并返回迭代器中的下一个数据。
我们使用iter
函数可以从任何可迭代对象中获取一个迭代器:
a = iter([1, 2, 3])
print(a)
print(next(a))
print(1 in a)
# 执行结果
<list_iterator object at 0x124111a50>
1
False
可以看到,我们使用iter
函数可以从任何可迭代对象中获取一个迭代器。而且迭代器有个特点,即每次用完一个元素即消耗掉该元素,不会保留在迭代器中,也就是说,是一次性的。
res = iter([1, 2, 3, 4])
print(next(res), end=" ")
print(next(res), end=" ")
print(next(res), end=" ")
print(next(res), end=" ")
# print(next(res))
r = list(res)
print(r)
# 执行结果
1 2 3 4 []
可以看到,list
里面已经空了。
我们用for
来取值:
res = iter([1, 2, 3, 4])
for i in res:
print(i, end=" ")
r = list(res)
print(r)
# 执行结果
1 2 3 4 []
for
直接将迭代内的元素全部取完了,所以最后打印下一个值的时候也显示空了。所以我们可以得到迭代器的取值方案:
next()
:调用一次获取一次,直到数据被取完。list()
:使用list
函数直接取出迭代器中的所有数据。for
:使用for
循环遍历迭代器的数据总结一下:
iter()
和 next()
。那么,再回过头来看看range
range
可以像任何其他可迭代对象一样循环使用,但是它并不具备迭代器中的一些特性,比如,我们之前实验过,range
并不能使用next
方法,而我们可以从range
中得到一个迭代器:
iter(range(3))
# 执行结果
<range_iterator at 0x124184570>
我们在迭代器中使用元素就会消耗掉该元素,但是我们遍历一个range
对象并不消耗它, 比如:
很明显我们可以重复使用。
来一个更直接的,我们之前用for
获取了迭代里的值,我们对range()
也来使用一下看看会不会有不同的结果:
res = range(1,5)
for i in res:
print(i, end=" ")
r = list(res)
print(r)
# 执行结果
1 2 3 4 [1, 2, 3, 4]
一样的代码,对象不同。我们可以明显看到区别,range
拿到最后里面的元素并没有减少。这也说明了,range
并不是迭代器。
实际上,range
的迭代是通过iter
协议来实现的,只是一种类似迭代器的鸭子类型,并非真正的迭代器。
其实,有一种可以直接检测迭代器和可迭代对象的方法:
# 检测迭代器和可迭代对象
from collections.abc import Iterator, Iterable
varstr = '123456'
res = iter(varstr)
r = range(1, 5)
# isinstance() 检测一个数据是不是一个指定的类型
# Iterable: 迭代对象,Iterator: 迭代器
r1 = isinstance(varstr, Iterable)
r2 = isinstance(varstr, Iterator)
r3 = isinstance(res, Iterable)
r4 = isinstance(res, Iterator)
r5 = isinstance(r, Iterable)
r6 = isinstance(r, Iterator)
print(f'varstr 是迭代对象:{r1}, \t varstr 是迭代器: {r2}')
print(f'res 是迭代对象:{r3}, \t res 是迭代器: {r4}')
print(f'r 是迭代对象:{r5}, \t r 是迭代器: {r6}')
# 执行结果
varstr 是迭代对象:True, varstr 是迭代器: False
res 是迭代对象:True, res 是迭代器: True
r 是迭代对象:True, r 是迭代器: False
今天的知识点讲到这就结束了,接下来,让我们来做两个小练习。
还记得我们之前讲过的斐波那契数列吗?不记得没关系,我们来复习一下:
# 斐波那契数列: 0, 1, 1, 2, 3, 5, 8, 13...
我们这次来实现一个函数,用于查询斐波那契数列中当前位置的数值是多少。
# 递归实现斐波那契数列
def fibonacci(n):
if n == 1:
return 0
elif n == 2 or n == 3:
return 1
else:
return fibonacci(n-1) + fibonacci(n-2)
res = fibonacci(6)
print(res)
# 执行结果
5
我为大家画了张图,来看看程序内部到底做了些什么:
从这张图中,我们可以看到递归的步骤和返回的结果。
什么是阶乘?比如我们实现7的阶乘,那么就是
让我们来试着实现一下:
# 实现阶乘
def factorial(n):
if n == 1:
return 1
else:
return n*factorial(n-1)
res = factorial(7)
print(res)
# 执行结果
5040
验证一下看看:
print(1*2*3*4*5*6*7)
# 执行结果
5040
看来结果没问题,那让我们来看看程序内发生了什么:
'''
factorial(7) =>
7 * factorial(6) =>
6 * factorial(5) =>
5 * factorial(4) =>
4 * factorial(3) =>
3 * factorial(2) =>
2 * factorial(1) =>
factorial(1) = 1
2 * 1 = 2
3 * 2 = 6
4 * 6 = 24
5 * 24 = 120
6 * 120 = 720
7 * 720 = 5040
'''
虽然实现了,最后还是不得不说几点注意事项:
递归函数的效率并不高,所以尽量能不用就不要用。
一个函数如果调用后没有结束,那么栈空间中就一直存在,直到这个函数运算结束才会销毁。
好了,今天的课程到此结束。大家课后记得多练习。下课!