《Python进阶系列》七:迭代器与生成器

Python中的迭代器与生成器

\quad
\quad
在Python中存在两种好用的功能:迭代器生成器。以list容器为例,在使用该容器迭代一组数据时,必须事先将所有数据存储到容器中,才能开始迭代;而生成器却不同,它可以实现在迭代的同时生成元素。

也就是说,对于可以用某种算法推算得到的多个数据,生成器并不会一次性生成它们,而是什么时候需要,才什么时候生成。
\quad

迭代器

迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退

Python中迭代器协议主要用到了两个魔法方法:__iter__(),__next__()

  • __iter__()方法创建一个具有__next__()方法的迭代器对象
  • __next__()方法返回下一个迭代器对象

只具有迭代操作的生成器,也是属于迭代器的。

字符串,列表或元组对象都可用于创建迭代器:

>>> list=[1,2,3,4]
>>> it = iter(list)    # 创建迭代器对象
>>> print (next(it))   # 输出迭代器的下一个元素
1
>>> print (next(it))
2

迭代器对象可以使用常规for语句进行遍历:

>>> list=[1, 2, 3, 4]
>>> it = iter(list)    # 创建迭代器对象
>>> for x in it:
>>>     print (x, end=" ")
1 2 3 4

\quad

创建迭代器

把一个类作为一个迭代器使用需要在类中实现两个方法 __iter__()__next__()

  • __iter__() 方法返回一个特殊的迭代器对象, 这个迭代器对象实现了__next__()方法并通过 StopIteration异常标识迭代的完成。
  • __next__() 方法会返回下一个迭代器对象。

创建一个返回数字的迭代器,初始值为 1,逐步递增 1:

class MyNumbers:
  def __iter__(self):
    self.a = 1
    return self
 
  def __next__(self):
    x = self.a
    self.a += 1
    return x
 
myclass = MyNumbers()
myiter = iter(myclass)
for x in myiter:
  print(x)

\quad

StopIteration

StopIteration异常用于标识迭代的完成,防止出现无限循环的情况,在__next__()方法中我们可以设置在完成指定循环次数后触发StopIteration异常来结束迭代。

在 20 次迭代后停止执行:

class MyNumbers:
  def __iter__(self):
    self.a = 1
    return self
 
  def __next__(self):
    if self.a <= 20:
      x = self.a
      self.a += 1
      return x
    else:
      raise StopIteration
 
myclass = MyNumbers()
myiter = iter(myclass)
for x in myiter:
  print(x)

\quad

生成器

生成器(generator)从字面意思上理解,就是循环计算的操作方式。在Python中,提供一种可以边循环边计算的机制。

生成器是解决使用序列存储大量数据时,内存消耗大的问题,而且可以避免不必要的计算,带来性能上的提升。我们可以根据存储数据的某些规律,演算为算法,在循环过程中通过计算得到,这样可以不用创建完整序列,从而大大节省占用空间。

跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。

在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值,并在下一次执行 next() 方法时从当前位置继续运行。

调用一个生成器函数,返回的是一个迭代器对象。
\quad

实现生成器的两种方式

第一种方法:把一个列表生成式的[]改成(),就创建了一个生成器。

>>> L=[x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x00000236B9435C10>

这里一定要注意把[]改成()后,不是生成一个tuple,而是生成一个generator。

第二种方法:在函数中使用yield关键字,函数就变成了一个生成器。调用该函数,就等于创建了一个生成器对象。

函数里有了yield后,执行到yield就会停住,当需要再往下算时才会再往下算。所以生成器函数即使是有无限循环也没关系,它需要算到多少就会算多少,不需要就不往下算。

一个生成器,主要是通过循环反复调用next()方法,直到捕获异常

来一个例子说明一下生成器的用法:

def intNum():
    print("开始执行")
    for i in range(5):
        yield i
        print("继续执行")
num = intNum()

和普通函数不同,intNum() 函数的返回值用的是yield关键字,而不是return关键字,此类函数就称为生成器函数。调用生成器函数,就可以创建一个 num 生成器对象。

生成器的使用有三种方法:

#调用 next() 内置函数
print(next(num))

#调用 __next__() 方法
print(num.__next__())

#通过for循环遍历生成器
for i in num: print(i)

程序执行结果:

开始执行
0
继续执行
1
继续执行
2
继续执行
3
继续执行
4
继续执行

程序的执行流程:

  1. 首先,在创建有num生成器的前提下,通过其调用next()内置函数,会使 Python 解释器开始执行 intNum() 生成器函数中的代码,因此会输出“开始执行”,程序会一直执行到yield i,而此时的 i=0,因此 Python 解释器输出“0”。由于受到 yield 的影响,程序会在此处暂停。
  2. 然后,我们使用 num 生成器调用 __next__() 方法,该方法的作用和 next() 函数完全相同(事实上,next() 函数的底层执行的也是 __next__() 方法),它会是程序继续执行,即输出“继续执行”,程序又会执行到yield i,此时 i=1,因此输出“1”,然后程序暂停。
  3. 最后,我们使用for循环遍历num生成器,之所以能这么做,是因为for循环底层会不断地调用next()函数,使暂停的程序继续执行,因此会输出后续的结果。
  4. 如果此时再调用next()函数,此时程序会报错,因为生成器执行完毕后辉捕捉异常。
Traceback (most recent call last):
  File "D:\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 3418, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-40-c3aa4ea10659>", line 1, in <module>
    next(num)
StopIteration

因此,带yield的函数具体内部执行操作为:

  • yield方法:相当于return作用,程序遇到yield则直接中止后续步骤;
  • 当再次调用生成器时,next()方法会唤醒,并继续执行yield后续步骤;
  • 还可以调用send()方法,可以唤醒,并传入一个值,继续执行yield后续步骤。

生成器是可迭代的,每一次只可读一次。因此常常与for循环一起组合使用。

\quad

生成器案例

\quad
案例1:“生成器是解决使用序列存储大量数据时,内存消耗大的问题”的代码证明:

import sys

def test(n):    
    print("start")    
    while n > 0:        
        yield n        
        n-=1    
    print("end")
        
a = [x for x in range(1000)]
b= test(1000)
print("a内存大小:",sys.getsizeof(a))
print("b内存大小:",sys.getsizeof(b))

程序执行结果:

a内存大小: 9016
b内存大小: 112

\quad
\quad
案例2:使用函数生成器打印斐波那契数列。

def fib(length):
    a, b = 0, 1
    n = 0
    while n < length:
        yield b
        a, b = b, a + b
        n += 1
    return '没有更多的元素了'

程序执行结果:

>>> g = fib(8)
>>> for i in g: print(i)
1
1
2
3
5
8
13
21

\quad
案例3send()方法

def gen():
    i = 0
    while i < 5:
        temp = yield i
        print('temp:', temp)
        i += 1
    return '没有更多的元素'

>>> g = gen()
>>> print(g.send(None))
>>> n1 = g.send('起点')
>>> print('n1', n1)
>>> n2 = g.send('发展')
>>> print('n2', n2)
0
temp: 起点
n1 1
temp: 发展
n2 2

\quad
案例4:应用多任务

先设置两个虚拟的任务函数

def task1(n):
   for i in range(n):
      print('正在搬第{}块砖!'.format(i))

def task2(n):
   for i in range(n):
      print('正在听第{}首歌!'.format(i))

现在分别执行这两个任务:

>>>  task1(10)
>>>  task2(5)

正在搬第0块砖!
正在搬第1块砖!
正在搬第2块砖!
正在搬第3块砖!
正在搬第4块砖!
正在搬第5块砖!
正在搬第6块砖!
正在搬第7块砖!
正在搬第8块砖!
正在搬第9块砖!
正在听第0首歌!
正在听第1首歌!
正在听第2首歌!
正在听第3首歌!
正在听第4首歌!

可以看到,任务并不是交替执行的(非多任务),而是先执行完一个任务,再执行下一个任务。现在用生成器来变成多任务执行。

>>> g1 = task1(10)
>>> g2 = task2(5)
>>> while True:
>>>    g1.__next__()
>>>    g2.__next__()

正在搬第0块砖!
正在听第0首歌!
正在搬第1块砖!
正在听第1首歌!
正在搬第2块砖!
正在听第2首歌!
正在搬第3块砖!
正在听第3首歌!
正在搬第4块砖!
正在听第4首歌!
正在搬第5块砖!
Traceback (most recent call last):
  File "task.py", line 16, in <module>
    g2.__next__()
StopIteration

报错用异常捕捉处理一下:

>>> g1 = task1(10)
>>> g2 = task2(5)
>>> while True:
>>>    try:
>>>       g1.__next__()
>>>       g2.__next__()
>>>    except:
>>>       pass

正在搬第0块砖!
正在听第0首歌!
正在搬第1块砖!
正在听第1首歌!
正在搬第2块砖!
正在听第2首歌!
正在搬第3块砖!
正在听第3首歌!
正在搬第4块砖!
正在听第4首歌!
正在搬第5块砖!
正在搬第6块砖!
正在搬第7块砖!
正在搬第8块砖!
正在搬第9块砖!

\quad

可迭代对象 VS 迭代器

可迭代对象(Iterable)是可以直接作用于for循环的对象。包括集合数据类型(list、tuple、dict、set、str等)和生成器(generator)。可以使用isinstance()判断一个对象是否是Iterable对象。

>>>from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False

迭代器(Iterator)表示的是一个数据流。Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

生成器都是Iterator对象,但listdictstr虽然是Iterable,却不是Iterator。把listdictstr等Iterable变成Iterator可以使用iter()函数:

>>> isinstance(iter([]),Iterator)
True
>>> isinstance( iter('abc'),Iterator)
True

Python的for循环本质上就是通过不断调用next()函数实现的,例如:

for x in [1, 2, 3, 4, 5]:
    pass

实际上完全等价于:

it= iter([12345])  # 获得Iterator对象
while True:
    try:
        x = next(it)  # 获得下一个值
    except StopIteration:
        break  # 遇到StopIteration就退出循环

\quad

itertools模块

python的内置模块itertools提供了用于操作迭代对象的函数,非常方便实用。举一个例子:

islice(iterable, [start], stop, [step])

创建一个迭代器,生成项的方式类似于切片返回值:iterable[start:stop:step],将跳过前start个项,迭代在stop所指定的位置停止,step指定用于跳过项的步幅。与切片不同,负值不会用于任何**start****stop****step**,如果省略了start,迭代将从0开始,如果省略了step,步幅将采用1。

举个例子:

from itertools import islice

def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b
 
>>> f = fib()
>>> print(list(islice(f, 10)))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

\quad

参考

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奋斗的西瓜瓜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值