第049讲:生成器

版权声明:转载请标明出处 https://blog.csdn.net/qq_41556318/article/details/84885697

目录

0. 请写下这一节课你学习到的内容:格式不限,回忆并复述是加强记忆的好方式!

测试题(笔试,不能上机哦~)

0. 通常,一般的函数从第一行代码开始执行,并在什么情况下结束?

1. 什么是协同程序?

2. 生成器所能实现的任何操作都可以由迭代器来代替吗,为什么?

3. 将一个函数改造为生成器,说白了就是把什么语句改为 yield 语句?

4. 说到底,生成器的最大作用是什么?

5. 如下,get_prime() 是一个获得素数的生成器,请问第 2 行代码 while True 有何作用?

动动手(一定要自己动手试试哦~)

0. 要求实现一个功能与 reversed() 相同(内置函数 reversed(seq) 是返回一个迭代器,是序列 seq 的逆序显示)的生成器。例如:

1. 10 以内的素数之和是:2 + 3 + 5 + 7 = 17,那么请编写程序,计算 2000000 以内的素数之和?


0. 请写下这一节课你学习到的内容:格式不限,回忆并复述是加强记忆的好方式!

因为上一节课给大家介绍了迭代器,这一节课继续给大家介绍生成器,虽然说生成器和迭代器可以说是Python近几年来引入的最强大的两个概念,但是生成器的学习并不涉及到高级的魔法方法,甚至巧妙的避开了类和对象,仅需要通过普通的函数就可以实现了,由于生成器的概念比较高级,所以在之前的函数章节并没有讲到它,因为那时候大家还只是基础阶段,学习就是需要一个渐进的过程。像上一节课的迭代器,很多人学了之后就感觉很简单,但是如果我们把迭代器放到循环那一章节来讲,大家势必就会一头雾水了。

正如刚才所说,生成器事实上是迭代器的一种实现,那既然迭代器可以实现,那为什么还要生成器呢?有一句老话说得好:“存在即合理”。生成器的发明一方面就是使得Python更加简洁,因为迭代器需要我们去定义一个类和实现相关的方法,才可以定义一个灵活的迭代器。而生成器需要在普通的函数加上一个 yield 语句。另外一个重要的方面就是生成器的发明使得Python模仿协同程序的概念得以实现。

所谓的协同程序就是可以运行的独立函数的调用,函数可以暂停或者挂起,并在需要的时候从程序离开的地方继续或者重新开始。我们知道,对于一个普通的函数来说,我们调用它一般都是从函数的第一句开始走,结束用 return 语句或者 异常 或者 函数所有的语句执行完毕。一旦函数将控制权交还给调用者,那就意味着全部都结束了,因为我们说过:函数的所有的工作、保存都是局部变量,局部变量在调用结束都会自动消失。

而生成器就是一个特殊的函数,调用可以中断、可以暂停,暂停之后他把控制权临时交出来,然后交完之后,在需要的时候他又能够获取回来,重新获得控制权,从上一次暂停的时候继续下去。这就是生成器(Generator)。

多说不如实干,给大家举个例子:

>>> def myGen():
	print("生成器被执行!")
	yield 1
	yield 2

	
>>> myG = myGen()
>>> next(myG)
生成器被执行!
1
>>> next(myG)
2
>>> next(myG)
Traceback (most recent call last):
  File "<pyshell#29>", line 1, in <module>
    next(myG)
StopIteration

一旦一个函数里面出现 yield 语句,那么这个函数就被定义为生成器,yield 相当于函数里面的 return 语句,但是普通函数的 return 一返回,这个函数就结束了,但是对于生成器来说,出现 yield,它就会把 yield 后面的参数给返回,然后就暂停在这个 yield 的位置,下一次执行就从这里开始。如程序执行所示,第一次 next() 时,就打印了 1,第二次 next() 就继续打印了 2,然后就没有了,如果继续 next() 就抛出了 StopIteration 的异常。

Python的 for 循环会自动调用 next() 方法和探测 StopIteration 结束,如下:

>>> for i in myGen():
	print(i)

	
生成器被执行!
1
2

我们上节课斐波那契数列的例子,也可以用生成器来实现:

>>> def fibs():
	a = 0
	b = 1
	while True:
		a, b = b, a + b
		yield a

看上去,好像是死循环,因为永远为True,但是这是一个生成器,随时都可以被暂停,只要 yield, 就返回,就暂停了。

我们用一个 for 语句把它打印出来,我们这里设置一下,如果大于100的话,就跳出循环,要不就会一直走下去:

>>> for each in fibs():
	if each > 100:
		break
	print(each, end = '  ')

	
1  1  2  3  5  8  13  21  34  55  89  

事到如今,我们应该已经很好的掌握了列表推导式:

>>> a = [i for i in range(100) if not(i % 2) and (i % 3)]
>>> a
[2, 4, 8, 10, 14, 16, 20, 22, 26, 28, 32, 34, 38, 40, 44, 46, 50, 52, 56, 58, 62, 64, 68, 70, 74, 76, 80, 82, 86, 88, 92, 94, 98]

在列表里加一个 for 语句就是列表推导式,上面的就是求100以内能被2整除,但是不能被3整除的整数。

这里想告诉大家的是,Python3 除了有列表推导式之外,还有字典推导式:

>>> b = {i : i % 2 == 0 for i in range(10)}
>>> b
{0: True, 1: False, 2: True, 3: False, 4: True, 5: False, 6: True, 7: False, 8: True, 9: False}

这可以得到0-9 这10个数字是否为偶数,如果是偶数,i % 2 == 0 就是True。

还有集合推导式:

>>> c = {i for i in [1, 2, 3, 2, 4, 3, 6, 5]}
>>> c
{1, 2, 3, 4, 5, 6}

照这种情形想下去,很多人就会以为会有字符串推导式和元组推导式。我们试一下:

>>> d = "i for i in 'I love Python!'"
>>> d
"i for i in 'I love Python!'"

显然,对于字符串推导式,我们是不可能实现的,因为出现双引号,它就会把里面的内容如实打印出来,因为这就是字符串。

那么对于所谓的元组推导式呢?

>>> e = (i for i in range(10))
>>> e
<generator object <genexpr> at 0x00000235B13EECA8>

显然,打印的不是元组(tuple),这里没有打印数据,但是出现了 generator (生成器),没错,这里我们铺垫了这么久,就是为了告诉大家,我们这里用圆括号括起来的,就是生成器推导式。我们来证明一下:

>>> next(e)
0
>>> next(e)
1
>>> next(e)
2
>>> for each in e:
	print(each)

	
3
4
5
6
7
8
9

生成器推导式如果作为函数的参数,可以直接写推导式就可以了,不用加圆括号,

例如,如果我们对上面的生成器推导式求和,应该是:

>>> sum((i for i in range(10)))
45

但是生成器推导式如果作为函数的参数,不用加圆括号也是可以的:

>>> sum(i for i in range(10))
45

但是单独的生成器推导式,是一定需要括号的,这是基本的语法要求:

>>> i for i in range(10)
SyntaxError: invalid syntax

关于生成器的技术要点,大家可以继续参阅 -> 解释 yield 和 Generators(生成器)


测试题(笔试,不能上机哦~)

0. 通常,一般的函数从第一行代码开始执行,并在什么情况下结束?

答:对于调用一个普通的 Python 函数,一般是从函数的第一行代码开始执行,结束于 return 语句、异常或者函数所有语句执行完毕。一旦函数将控制权交还给调用者,就意味着全部结束。函数中做的所有工作以及保存在局部变量中的数据都将丢失。如果再次调用这个函数时,一切都将重新开始。

1. 什么是协同程序?

答:所谓的协同程序就是可以运行的独立函数调用,函数可以暂停或者挂起,并在需要的时候从程序离开的地方继续或者重新开始。

Python 是通过生成器来实现类似于协同程序的概念:生成器可以暂时挂起函数,并保留函数的局部变量等数据,然后在再次调用它的时候,从上次暂停的位置继续执行下去。

2. 生成器所能实现的任何操作都可以由迭代器来代替吗,为什么?

答:是的,都可以。因为生成器事实上就是基于迭代器来实现的,生成器只需要一个 yield 语句即可,但它内部会自动创建 __iter__() 和 __next__() 方法。

3. 将一个函数改造为生成器,说白了就是把什么语句改为 yield 语句?

答:return 语句。

4. 说到底,生成器的最大作用是什么?

答:使得函数可以“保留现场”,当下一次执行该函数是从上一次结束的地方开始,而不是重头再来。

5. 如下,get_prime() 是一个获得素数的生成器,请问第 2 行代码 while True 有何作用?

def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1

答:这个 while True 循环是用来确保生成器函数永远也不会执行到函数末尾的。只要调用 next() 这个生成器就会生成一个值。这是一个处理无穷序列的常见方法(这类生成器也是很常见的)。


动动手(一定要自己动手试试哦~)

0. 要求实现一个功能与 reversed() 相同(内置函数 reversed(seq) 是返回一个迭代器,是序列 seq 的逆序显示)的生成器。例如:

>>> for i in myRev("FishC"):
    print(i, end='')

ChsiF

代码清单:

def myRev(data):
    # 这里用 range 生成 data 的倒序索引
    # 注意,range 的结束位置是不包含的
    for index in range(len(data)-1, -1, -1):
        yield data[index]

1. 10 以内的素数之和是:2 + 3 + 5 + 7 = 17,那么请编写程序,计算 2000000 以内的素数之和?

答:如果你的策略是将 2000000 以内的所有素数找到并存放到一个列表中,再依次进行求和计算,那么这个列表极有可能会撑爆你的内存。所以这道题就必须用到生成器来实现啦。

详细解释请参考:解释 yield 和 Generators(生成器)

结果是 142913828922,你答对了吗?!

代码清单:

import math

def is_prime(number):
    if number > 1:
        if number == 2:
            return True
        if number % 2 == 0:
            return False
        for current in range(3, int(math.sqrt(number) + 1), 2):
            if number % current == 0:
                return False
        return True
    return False

def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1

def solve():
    total = 2
    for next_prime in get_primes(3):
        if next_prime < 2000000:
            total += next_prime
        else:
            print(total)
            return

if __name__ == '__main__':
    solve()

猜你喜欢

转载自blog.csdn.net/qq_41556318/article/details/84885697
今日推荐