Python面试题之Python生成器
首先说明一下生成器也是迭代器,也有迭代器的那些优点。
那为什么要生成器呢?因为到目前为止都 不是你写的迭代器,都是别人定义好的。那如何自己去造一个迭代器呢?下面的内容就会给你答案。
想要自己造一个迭代器,我们可以根据迭代器的特征(只要一个对象有__iter__
和__next__
方法那它就是迭代器),自己定义一个类,然后定义一个__iter__()
和__next__()
, 然后这个类实例化的对象就是一个迭代器啦。
但是这样写太麻烦啦!何况我们现在还没有学到类的知识,怎么办?给你一个魔法棒,让你快速优雅高效地造一个迭代器。
yield关键字
第一种自造迭代器的方法就是使用yield
关键字。具体怎么实现呢?
非常简单,如下所示:
def g(): print("Hey~ 生成器") yield 1
上面的写法非常类似于函数的定义,相当于把return换成了yield
(当然,并没有这么简单)。
此时,我执行g()
返回的就是一个生成器。就是这么简单。
ret = g() print(ret) #输出<generator object g at 0x101fef6d0>
但是这里有个特别需要注意的地方,也是与函数最明显的区别:
我们执行g()
的时候,并没有打印"Hey~ 生成器"
,就像函数没执行一样。
这也是生成器一个非常重要的特点,那就是你执行g()
返回的是一个生成器,同时只有在迭代它(调用它的__next__()
)的时候它才开始执行内部代码,碰到yield
关键字就返回yield后面的值并停止。
print(ret.__next__()) $print(next(ret))
输出:
Hey~ 生成器
1
当然for循环它也是可以的:
for i in ret: print(i)
输出:
Hey~ 生成器
1
yield
还可以多次执行,这与return也有区别。
def g2(): print("Hey~ 生成器1") yield 1 print("Hey~ 生成器2") yield 2 ret = g2()
此时,你执行下next(ret)
,会打印"嘿!生成器1"
,然后返回一个1
,再执行一次next(ret)
,会打印出"嘿!生成器2"
,然后返回一个2
。
print(next(ret)) # 输出 Hey~ 生成器1 1 print(next(ret)) # 输出 Hey~ 生成器2 2
yield与return的区别
在一个函数里return只能执行一次,return
之后函数就彻底结束了:
def test_return(): return 1 return 2 #永不执行 return 3 #永不执行
yield
之后可以保存函数的运行状态,下次继续执行:
def test_yield(): yield 1 yield 2 #下次next()后执行 yeild 3 #下次next()后执行
下面的例子中,使用return
时,只能返回0
。
def test_return2(): for i in range(10): return i #只能返回0,函数就结束了
使用yield
能够依次返回0~9
。
def test_yield2(): for i in range(10): yield i #每调用一次next()就会一次弹出0~9
yield的作用
yield
把函数变成了生成器(生成器就是迭代器)。- 为函数封装好了
__iter__
和__next__
方法,把函数的执行结果做成了迭代器。 - 遵循迭代器的取值方式 —
obj.__next__()
,触发的是函数的执行。函数暂停与继续执行的状态都是由yield
保存的。
倒计时的例子:
def countdown(n): print("倒计时开始") while n > 0: yield n n -= 1 print("发射")
分析下面语句的执行过程:
g = countdown(5) print(g.__next__()) # 打印"倒计时开始" 返回5 (此时n=5) print(g.__next__()) # 返回4 (此时n=4) print(g.__next__()) # 返回3 (此时n=3) print(g.__next__()) # 返回2 (此时n=2) print(g.__next__()) # 返回1 (此时n=1) print(g.__next__()) # 打印"发射" 抛出StopIteration异常(此时n=0)
调用__next__()
时函数执行内部代码,到yield
关键字时暂停:
g = countdown(5) print(g.__next__())
输出:
倒计时开始
5
生成器也是不能后退:
g = countdown(5) print(g.__next__()) print(g.__next__()) for i in g: print(i)
输出:
倒计时开始 5 4 -- for -- 3 2 1 发射
每调用一次countdown(5)
得到的都是不同的生成器
for i in countdown(5): print(i) for i in copuntdown(5): print(i)
输出:
5 5
下面的例子也是一样,每一次print中countdown(5)
都是一个全新的生成器,所以打印出来的值都是5
。
print(countdown(5).__next()) print(countdown(5).__next()) print(countdown(5).__next())
输出:
5 5 5
生成器表达式
我们之前学过列表推导式,是这样写的:
>>> [i for i in range(10)] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
这样来得到一个元素数量较小的列表是非常方便的,但是如果要创建一个元素数量巨大的列表,就不那么友好了
>>> [i for i in range(10000000000)] ...
这个时候只要把[]
换成()
就把列表推导式 变成了生成器表达式,得到的就是一个生成器对象,就是这么神奇。
这就是第二种自造迭代器的方法。
>>> (i for i in range(10)) <generator object <genexpr> at 0x101fef6d0>
我们可以直接使用for循环遍历上面得到的生成器:
>>> for i in (i for i in range(10)): ... print(i) ... 0 1 2 3 4 5 6 7 8 9
这样我们就能自信的创建个10000000000元素的生成器,不担心内存会爆了。
>>> (i for i in range(10000000000)) <generator object <genexpr> at 0x101fef728>
最后的总结: