Python中的迭代器与生成器

news/2025/2/12 4:37:45/

Python中的迭代器与生成器

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

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

迭代器

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

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

创建迭代器

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

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

__next__() 方法会返回下一个迭代器对象。

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

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

StopIteration

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

在 20 次迭代后停止执行:

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

生成器

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

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

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

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

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

实现生成器的两种方式

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

>>> 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 iprint("继续执行")
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 的影响,程序会在此处暂停。

  1. 然后,我们使用 num 生成器调用 __next__() 方法,该方法的作用和 next() 函数完全相同(事实上,next() 函数的底层执行的也是 __next__() 方法),它会是程序继续执行,即输出“继续执行”,程序又会执行到yield i,此时 i=1,因此输出“1”,然后程序暂停。

  1. 最后,我们使用for循环遍历num生成器,之所以能这么做,是因为for循环底层会不断地调用next()函数,使暂停的程序继续执行,因此会输出后续的结果。

  1. 如果此时再调用next()函数,此时程序会报错,因为生成器执行完毕后辉捕捉异常。

Traceback (most recent call last):File "D:\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 3418, in run_codeexec(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循环一起组合使用。

生成器案例

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

import sysdef 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

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

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

程序执行结果:

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

案例3send()方法

def gen():i = 0while i < 5:temp = yield iprint('temp:', temp)i += 1return '没有更多的元素'>>> 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

案例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块砖!

可迭代对象 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对象,但list、dict、str虽然是Iterable,却不是Iterator。把list、dict、str等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([1,2,3,4,5])  # 获得Iterator对象
while True:try:x = next(it)  # 获得下一个值except StopIteration:break  # 遇到StopIteration就退出循环

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 islicedef fib():a, b = 0, 1while True:yield aa, b = b, a + b>>> f = fib()
>>> print(list(islice(f, 10)))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]


http://www.ppmy.cn/news/20549.html

相关文章

帮助粉丝用青泥学术大数据推荐毕业论文选题(围绕 教育信息化2.0、疫情期间线上学习质量问题、Steam教育、智慧教育等突破点来抉择)

需求 本科论文水平&#xff0c;青泥学术可以起到一定帮助。 说明 我也只是读了一个学期的硕士而已&#xff0c;谈不上多高的指点&#xff0c;可能比一些人更努力一些。 所以我的学术造诣不算太高&#xff0c;不敢盲目建议。 但是君子性非异也&#xff0c;善假于物也。 我借…

图文详解:箭头函数与常规函数的this指向问题

函数中this的指向问题特别容易让人迷糊&#xff0c;这里用示例来指点迷津&#xff0c;走出迷茫。 常规函数下的this指向 1. 纯粹的函数调用 function test(name) { console.log(name) console.log(this) } test(zjcopy) ; test.call(zjcopy, cuclife-2) ; test.call(fal…

【奇妙的数据结构世界】用图像和代码对队列的使用进行透彻学习 | C++

第十一章 队列 目录 第十一章 队列 ●前言 ●一、队列是什么&#xff1f; 1.简要介绍 2.具体情况 ●二、队列操作的关键代码段 1.类型定义 2.顺序队列的常用操作 3.链式队列的常用操作 ●总结 前言 简单来说&#xff0c;数据结构是一种辅助程序设计并且进行优化的方法论&…

JavaScript的基础知识

目录 一、初识JavaScript 二、JavaScript的基础 1、初步了解 2、代码位置 3、注释 4、变量 ①字符串 ②数组 ③对象 ④条件语句 ⑤函数 三、DOM模块 一、初识JavaScript JavaScript&#xff0c;是一门编程语言。浏览器就是JavaScript语言的解释器。DOM和BOM 相当于编…

(六)redis持久化操作(RDBAOF)

目录 一、RDB&#xff08;Redis DataBase&#xff09; 1、简介 2、持久化流程 3、dump.rdb文件 4、配置文件 5、rdb的备份 6、rdb的恢复 7、优势 8、劣势 二、AOF&#xff08;Append Only File&#xff09; 1、简介 2、持久化流程 3、AOF和RDB同时开启 4、AOF启动…

Ragnar-lothbrok靶机总结

Ragnar-lothbrok靶机渗透总结 靶机下载地址: https://download.vulnhub.com/ragnarlothbrok/Ragnar-lothbrok.ova 打开靶机,使用nmap扫描出靶机的ip和所有开放的端口 可以看到靶机开放了21端口,80端口,443端口,3306端口 一般开放21端口,我们可以先尝试ftp的匿名登录 可以看到…

连续函数的运算与初等函数的连续性——“高等数学”

各位CSDN的uu们你们好呀&#xff0c;今天&#xff0c;小雅兰的内容是连续函数的运算与初等函数的连续性&#xff0c;上篇博客我们学到了函数的连续性和间断点&#xff0c;这篇博客相当于是上篇博客的一个补充&#xff0c;好吧&#xff0c;现在就让我们进入高等数学的世界吧 一、…

RocketMQ 原理介绍及详细安装教程

一、为什么选择RocketMQ Apache RocketMQ 自诞生以来&#xff0c;因其架构简单、业务功能丰富、具备极强可扩展性等特点被众多企业开发者以及云厂商广泛采用。历经十余年的大规模场景打磨&#xff0c;RocketMQ 已经成为业内共识的金融级可靠业务消息首选方案&#xff0c;被广泛…