前言
函数一般运行结束或遇到return语句,意味着函数运行结束,函数中做的所有工作,以及保存在局部变量中的数据也都将会丢失,再次调用这个函数的时候,一切从头再来。有没有办法让函数在退出之后还能保留状态呢?
有没有办法让函数在退出之后还能保留状态呢?
闭包、全局变量和生成器,但过多的使用全局变量会污染命名空间,闭包德定义相对复杂。简单的方法为生成器。
1.生成器的定义与举例
生成器就是在函数中使用yield表达式来代替return语句就可以了。
例1:
>>> def counter():
i=0
while i<=5:
yield i
i+=1
>>>
>>> counter() #得到的不是返回值,而是生成器。
>>>
>>> for i in counter(): # 生成器的使用
print(i)
0
1
2
3
4
5
>>>
代码解析: for语句它是从一个可迭代对象里面每次获取一个数据,counter生成器它的作用就是每次调用的时候提供一个数据,每次执行到yield的时候,就生成一个数据,暂停并保留状态,下一次调用则从下一个语句i+=1开始继续执行。
生成器不像列表、元祖这些可迭代对象,可以把生成器看作一个制作机器,它的作用就是每调用依次提供一个数据,并且会记住当时的状态,而列表元祖这些可迭代对象则是容器,它们里面存放的是早已经准备好的所有数据。生成器其实可以看作是一种特殊的迭代器,其次它支持next()函数。
生成器每调用一次返回一个结果,导致生成器对象是无法使用下标索引,这种随机访问的方式的。
# 对生成器进行赋值
>>> c=counter()
>>> c
>>> next(c)
0
>>> next(c)
1
>>> next(c)
2
>>> next(c)
3
>>> next(c)
4
>>> next(c)
5
>>> next(c)
Traceback (most recent call last):
File "", line 1, in
next(c)
StopIteration
>>>
>>> c[2] #报错,生成器不支持下标索引的方式
Traceback (most recent call last):
File "", line 1, in
c[2]
TypeError: 'generator' object is not subscriptable
>>>
2.生成器的使用
生成器在斐波那契数列的使用。
斐波那契数列数列由0和1开始,之后的斐波那契数列就是由之前的两数相加得出:0,1,1,2,3,5,8,…
#由于在函数中没有设置结束条件,这样我们就得到了一个永恒的斐波那契数列生成器。
>>> def fib():
back1,back2=0,1
while True:
yield back1
back1,back2=back2,back1+back2
>>> f=fib()
>>> next(f)
0
>>> next(f)
1
>>> next(f)
1
>>> next(f)
2
>>> next(f)
3
>>> next(f)
5
>>> next(f)
8
使用for语句迭代,而没有在中途设置退出条件的话,那么就会得到一个一直执行下去的数列,直到按ctrl+c退出。
>>> for i in f:
print(i)
13
21
34
55
89
144
233
377
610
987
1597
Traceback (most recent call last):
File "", line 2, in
print(i)
KeyboardInterrupt
>>>
3.生成器表达式
将列表推导式的方括号改成圆括号会得到一个生成器表达式。
列表推导式会一下子将所有的数据生产出来,并放到一个列表中,但生成器就像一只母鸡,一次只下一个蛋。
>>> (i ** 2 for i in range(10)) # 得到一个生成器对象
at 0x00000160FDCE1570>
>>> t=(i ** 2 for i in range(10))
>>> next(t)
0
>>> next(t)
1
>>> next(t)
4
>>> next(t)
9
>>> next(t)
16
>>> for i in t:
print(i)
25
36
49
64
81
>>>
4.生成器总结
产生生成器的两种方法:一个就是将普通函数里面的return替换成yield表达式,另一个是直接使用这个生成器表达式。
题目:
1.迭代器可以看作是一种特殊的生成器吗?
答:不可以!
解析:生成器可以看成是一种特殊的迭代器,但反过来说就错了~
2.生成器可以看作是 Python 对于 “延迟执行” 提供的技术支持,这种说法正确吗?
答:正确。
解析:在需要的时候才产生结果,而并非一次性生产,这就是 “延迟执行” 的含义。当然,通过闭包,我们也可以实现类似的效果,不过生成器毕竟是 Python 帮我们定义好的语句,用起来肯定要顺手方便很多。
3. 你觉得生成器通常应用在什么场景会比较合适?
答:生成器应该应用在占用大量存储空间的文件操作上,比方说日志文件通常都特别大,几十上百 G 都算是小弟弟了,如果一次性把它读取到内存中,可想而知结果就是爆内存;另外一个应用场景就是希望输出的结果不断更新的情况,比如生成斐波那契数列,我们希望它每次产生的结果是基于上次的数据进行再运算产生。
4. 请问下面代码会打印 “YES” 吗?
>>> def counter():
... i = 0
... while i <= 5:
... yield i
... i += 1
... print("YES")
...
>>> for each in counter():
... print(each)
...
答:会。
打印结果如下:
>>> for each in counter():
... print(each)
...
0
1
2
3
4
5
YES
解析:这个就跟 return 语句不一样了,return 语句一执行,就不会管后面的语句了,yield 语句是 “打断并返回,然后下次执行就从下一条语句继续”。
5.下面这个列表推导式是不是有什么问题?为什么一直不出结果,你有什么办法解决这个问题么?
>>> [x ** 2 for x in range(1000000)]
答:
>>> (x ** 2 for x in range(1000000))
解析:对于百万级运算,生成器(表达式)对比列表推导式的优势就显而易见了;上面列表推导式代码,在多数电脑是要计算很久才能得到结果,因为它是实打实地在计算的;而生成器表达式则是将算法保存起来,但并没有实际去计算,所以就可以做到 “秒回”。不过,生成器表达式也并非没有缺点,它的缺点就是不支持随机访问,也就是下标索引……
6.请将下面的 map() 函数实现改为使用生成器表达式实现。
>>> list(map(abs, (-1, 2, -3, 4, -5)))
[1, 2, 3, 4, 5]
答:
>>> list(abs(x) for x in (-1, 2, -3, 4, -5))
[1, 2, 3, 4, 5]
解析:如果将生成器表达式作为函数的参数,最外层的小括号是可选的。
7.请将下面的生成器表达式实现改为使用 filter() 函数实现。
>>> "".join(x for x in "FishC" if x.isupper())
'FC'
答:
>>> "".join(filter(lambda x : x.isupper(), "FishC"))
'FC'
8.请问下面代码会打印什么内容?
list(map((lambda x : x ** 2), filter((lambda x : x % 2 == 0), range(10))))
答:
>>> list(map((lambda x : x ** 2), filter((lambda x : x % 2 == 0), range(10))))
[0, 4, 16, 36, 64]
解析:
从小括号的匹配结构上来看,不难看出,list() 函数的参数接受的可迭代对象由 map() 函数产生。
map() 函数的第一个参数应该是一个函数对象,这里用了 lambda 表达式,功能是将拿到的数据进行 2 次幂运算;那么第二个参数就是数据的来源,由 filter() 函数提供。
filter() 函数第一个参数也应该是一个函数对象,这里也用了 lambda 表达式,功能是筛选出能被 2 整除的数据;那么第二个参数就是数据的来源,是由 range(10) 获得,后者得到的是 0、1、2、3、4、5、6、7、8、9。其中能被 2 整除的就是 0、2、4、6、8,因此结果就是 [0, 4, 16, 36, 64]。
题目来自小甲鱼函数(VIII)