Python 中的生成器实现原理是什么?
创始人
2025-05-29 01:03:13
0次

本文首发自「慕课网」,想了解更多IT干货内容,程序员圈内热闻,欢迎关注!

作者| 慕课网精英讲师 朱广蔚

1. 如何生成一个巨大的序列

1.1 需求描述

要求生成一个包含很多元素的序列,假设:

  • 存储 1 个整数需要 4 个字节
  • 现在要创建一个包含 1 G 个整数的序列,从 0 到 1 * 1024 * 1024 * 1024 - 1
  • 如果需要为序列中的每个整数分配内存,则需要分配的内存为 1G * 4 = 4G

1.2 通过列表推导

Python 提供了列表推导用于生成列表,下面使用列表推导生成一个包含 0 到 4 之间所有整数的列表,代码如下:

>>> list = [i for i in range(4)]
>>> list
[0, 1, 2, 3]
代码块123
  • 在第 1 行,使用列表推导创建一个包含 4 个元素的列表
  • 在第 2 行,显示新创建的列表
  • 在第 3 行,创建了一个包含 0、1、2、3 等 4 个元素的列表

如果生成一个从 0 到 1G 的列表,代码如下:

>>> N = 1024 * 1024 * 1024
>>> list = [i for i in range(N)]
Traceback (most recent call last):File "", line 1, in File "", line 1, in 
MemoryError
代码块123456
  • 在第 1 行,设定 N 为 1G
  • 在第 2 行,使用列表推导创建一个包含 N 个元素的列表
  • 在第 6 行,程序运行出错,提示 MemoryError

使用列表推导创建包含 1G 个整数的列表时,需要为这 1G 个整数分配至少 4G 的内存,需要消耗大量的内存,超出了 Python 的限制,因此出现了 MemoryError 的错误。

另外,创建这个巨大的列表需要消耗大量的时间,因此执行第 2 行的语句后,系统失去响应,大约 10 多秒后才出现错误信息。

1.3 通过动态计算

列表推导需要一次性的为 1G 个整数分配内存空间,带来了两个问题:

  1. 列表占用了大量的物理内存
  2. 创建列表的时间过长

Python 提供了一种动态计算的思路解决以上问题,它的思想如下:

  1. 要生成的序列是有规则的,在这个例子中,要求生成连续递增的序列
  2. 使用一个特殊的对象 generator,该对象被称为生成器 generator,生成器按照规则依次输出该序列
  3. Python 提供了内置方法 next(generator),该方法通知生成器产生下一个数据并返回该数据
  4. 不需要为 generator 预先分配内存,通过调用 next(generator) 可以动态获取序列的下一个数据

创建一个输出从 0 到 1G 的生成器,代码如下:

>>> N = 1024 * 1024 * 1024
>>> generator = (i for i in range(N))
>>> next(generator)
0
>>> next(generator)
1
>>> next(generator)
2
代码块12345678
  • 在第 1 行,设定 N 为 1G
  • 在第 2 行,使用类似于列表推导的语法创建一个生成器,它输出从 0 到 1G 的序列注意:创建生成器的语法采用小括号 (),创建列表的语法采用方括号 []
  • 在第 3 行,使用 next(generator),通知 generator 生产一个数据在第 4 行,generator 输出从 0 到 1G 序列中的第 0 个整数
  • 在第 5 行,使用 next(generator),通知 generator 生产一个数据在第 6 行,generator 输出从 0 到 1G 序列中的第 1 个整数
  • 在第 7 行,使用 next(generator),通知 generator 生产一个数据在第 8 行,generator 输出从 0 到 1G 序列中的第 2 个整数

注意:在第 2 行,创建一个输出从 0 到 1G 的序列的生成器,因为不需要分配内存,创建生成器的速度非常快,几乎是瞬间完成的。与之相比,在上一节中创建一个输出从 0 到 1G 的序列的列表,因为需要分配内存,创建列表的速度非常慢,并且导致了 MemoryError。

2. 生成器概述

2.1 生成器的定义

在 Python 中,生成器是一个特殊的对象,它按照一定的规则依次输出数据。Python 的内置函数 next(generator) 通知生成器输出一个新的数据,当生成器输出全部数据后,产生一个特殊的异常 StopIteration,用于标记生成器输出结束。

下面的代码创建一个产生 0 到 3 之间所有整数的生成器:

>>> generator = (i for i in range(3))
>>> next(generator)
0
>>> next(generator)
1
>>> next(generator)
2
>>> next(generator)
Traceback (most recent call last):File "", line 1, in 
StopIteration
代码块1234567891011
  • 在第 1 行,创建一个产生 0 到 3 之间所有整数的生成器注意:创建生成器的语法采用小括号 (),创建列表的语法采用方括号 []
  • 在第 2 行,生成器产生第 0 个整数
  • 在第 4 行,生成器产生第 1 个整数
  • 在第 6 行,生成器产生第 2 个整数
  • 在第 8 行,生成器产生第 3 个整数在第 11 行,因为生成器生成的序列只包含 3 个整数,此时已经生成全部的整数,因此抛出异常 StopIteration

2.2 使用 while 循环访问生成器

根据生成器的原理,可以循环的调用 next(generator) 输出全部的序列,示例如下:

generator = (i for i in range(3))while True:try:item = next(generator)print(item)except StopIteration:break
代码块12345678
  • 在第 1 行,创建一个产生 0 到 3 之间所有整数的生成器
  • 在第 3 行,创建一个循环在第 5 行,调用 next(generator) 通知生成器返回一个数据在第 7 行,当生成器输出结束后,抛出异常 StopIteration

运行程序,输出结果如下:

0
1
2
代码块123

2.3 使用 for 循环访问生成器

通常使用 for 循环访问生成器,示例如下:

generator = (i for i in range(3))for item in generator:print(item)
代码块1234
  • 在第 1 行,创建一个产生 0 到 3 之间所有整数的生成器
  • 在第 3 行,使用 for 循环访问生成器

运行程序,输出结果如下:

0
1
2
代码块123

3. 创建生成器

3.1 通过推导创建生成器

可以使用类似于列表推导的语法创建一个生成器,语法如下:

(expression for i in iterable)
代码块1

该生成器遍历对象 iterable,依次产生数据 expression,它的工作流程如下:

for i in iterable:generate expression
代码块12

注意:创建生成器的语法与列表推导的语法相似,不同之处在于,创建生成器的语法采用小括号 (),创建列表的语法采用方括号 []。

通过推导创建生成器的示例如下:

generator = (i*2 for i in range(5))
for i in generator:print(i)
代码块123
  • 循环变量 i 从 0 变化到 4
  • 生成器每次产生数据 i*2

运行程序,输出结果如下:

0
2
4
6
8
代码块12345

3.2 通过复杂的推导创建生成器

可以使用类似于列表推导的语法创建一个生成器,语法如下:

(expression for i in iterable if condition)
代码块1

该生成器遍历对象 iterable,如果条件 condition 为真,则产生数据 expression,它的工作流程如下:

for i in iterable:if condition:generate expression
代码块123

通过复杂推导创建生成器的示例如下:

generator = (i for i in range(10) if i % 2 == 0)
for i in generator:print(i)
代码块123
  • 循环变量 i 从 0 变化到 9
  • 如果 i % 2 == 0,表示 i 是偶数
  • 生成器每次产生从 0 到 9 之间的偶数

运行程序,输出结果如下:

0
2
4
6
8
代码块12345

3.3 通过 yield 创建生成器

在生成器的生命周期中,生成器根据一定的规则产生一系列的数据,生成器可以使用 yield 关键字产生一个数据。例如,一个生成特定范围内的奇数序列的函数:

def generateOddNumbers(n):for i in range(n):if i % 2 == 1:yield igenerator = generateOddNumbers(10)
for i in generator:print(i)
代码块12345678
  • 在第 1 行,定义了函数 generateOddNumbers(n),它返回一个生成器,该生成器产生从 0 到 n 范围内的奇数
  • 在第 2 行到第 4 行,使用 for 循环生成从 0 到 n 范围内的奇数在第 3 行,如果 i % 2 == 1 为真,表示 i 是奇数在第 4 行,使用 yield i 生成一个数据 i
  • 在第 6 行,generateOddNumbers(10) 返回一个生成器,该生成器产生从 0 到 10 范围内的奇数
  • 在第 7 行,使用 for 循环遍历该生成器

运行该程序,输出如下:

1
3
5
7
9
代码块12345

注意:包含 yield 关键字的函数被称为生成器函数,调用生成器函数会返回一个生成器。在上面的例子中,函数 generateOddNumbers(n) 包含 yield 关键字,是一个生成器函数,它返回一个生成器,该生成器产生从 0 到 n 范围内的奇数。

4. 使用 yield 实现遍历堆栈的生成器

4.1 通过单链表实现堆栈

通过单链表实现堆栈,图示如下:

![图片描述](
//img.mukewang.com/wiki/5ea92d460974644d07000133.jpg)

在上图中,每个节点有两个字段: item 和 next,item 用于存储数据,next 指向下一个节点,head 指针指向堆栈的顶部。描述堆栈的 Python 代码如下:

class Node:def __init__(self, item):self.item = itemself.next = Noneclass Stack:def __init__(self):self.head = Nonedef push(self, item):node = Node(item)node.next = self.headself.head = nodestack = Stack()
stack.push('a')
stack.push('b')
stack.push('c')
代码块123456789101112131415161718
  • 在第 1 行,定义了类 Node 用于描述链表中的节点
  • 在第 6 行,定义了类 Stack 描述堆栈在第 8 行,定义了头指针 head,指向链表中的首个节点在第 10 行,定义了成员方法 push,将元素压如到堆栈中在第 11 行,创建一个新节点 node在第 12 行,新节点 node 的 next 指向头结点在第 13 行,头结点指向新节点
  • 在第 15 行,创建一个对象 stack
  • 在第 16 行到第 18 行,依次压入 3 个元素 ‘a’、‘b’、‘c’

4.2 使用 yield 关键字实现生成器函数

def stackGenerate(stack):cursor = stack.headwhile cursor != None:yield cursor.itemcursor = cursor.next
代码块12345
  • 在第 1 行,定义函数 stackGenerate(stack)该函数包含 yield 关键字,是一个生成器函数,它返回一个生成器生成器遍历堆栈,按出栈的顺序输出数据
  • 在第 2 行,变量 cursor 指向了当前正在遍历的元素,初始化被设置为链表的头结点
  • 在第 3 行,使用循环遍历堆栈如果变量 cursor 等于 None,表示已经到达链表的尾部,即遍历完全部的元素了在第 4 行,使用 yield 输出当前正在遍历的元素在第 5 行,将 cursor 指向下一个元素

4.3 通过 while 循环遍历堆栈

使用 while 循环显式的使用 next、StopIteration 完成对 stack 的遍历,代码如下:

generator = stackGenerate(stack)
while True:try:item = next(generator)print(item)except StopIteration:break
代码块1234567
  • 在第 1 行,stackGenerate(stack) 返回一个遍历堆栈的生成器
  • 在第 4 行,next(generator) 获取生成器的输出
  • 在第 6 行,当生成器输出结束后,抛出异常 StopIteration

程序依次压入 ‘a’、‘b’、‘c’,遍历时以压入相反的顺序输出,结果如下:

c
b
a
代码块123

4.4 通过 for … in 循环遍历堆栈

通过 for … in 循环对生成器进行遍历,代码如下:

generator = stackGenerate(stack)
for item in generator:print(item)
代码块123

与上一节的代码相比,代码要简洁很多,程序输出相同的结果如下:

c
b
a
代码块123

欢迎关注「慕课网」,发现更多IT圈优质内容,分享干货知识,帮助你成为更好的程序员!

相关内容

热门资讯

在大吉订购APP订购交易亏损被...   近期,不少投资者反映在“大吉订购App”上投资白银、铂金等商品遭遇严重亏损。该平台宣称提供现货订...
铂银壹号APP商品订购交易亏损...   许多中小投资者误以为网上的“商品现货订购”是合规合法的理财渠道,殊不知这背后隐藏着巨大的合规风险...
兴鑫淘金APP贵金属订购交易亏...   白银、贵金属或其他商品类投资,除国家批准的正规期货交易场所及持牌期货公司外,市面上大量第三方交易...
铂银掘金APP无资质的违规平台...   铂银掘金等平台的投资者,往往是被“注册送现金”“充值返现券”“首单免亏”等广告话术吸引,进而下载...
八方淘金APP订购交易不合规,...   “八方淘金APP”“铂银掘金APP”“铂银壹号APP”等软件被指以“现货订购”为名,实际采用高杠...
在指尖订购APP订购商品亏损被...   指尖订购APP这类平台,表面是“订购交易”,实则是披着电商外衣的非法期货陷阱。绝大多数投资者被蒙...
在犀贝易购APP做商品订购交易...   市面上打着“现货投资”“贵金属交易”旗号的APP并不少,但真正合规、受监管的平台并不多。犀贝易购...
易达淘金APP虚假商品订购交易...   易达淘金APP存在严重的虚假宣传与违规交易行为。该平台打着“商品订购”的旗号,实则通过不合规的交...
大浪淘金APP白银订购亏损被骗...   大浪淘金APP最隐蔽的套路,在于其完全封闭的内盘交易系统。表面上,它宣称与国际贵金属市场接轨;实...
一品好购APP上订购白银出现亏...   一品好购APP原名“一品订购”,运营方为上海宝智未来信息技术有限公司。近期不少投资者反映在该平台...
九州订购APP贵金属订购投资骗...   九州订购APP正通过抖.音、快手、今日头条等平台大量投放投资广告,用“5元起投”“充值返现”“0...
坚锅淘金APP交易软件并不正规...   当前,市面上大量所谓的“现货商城订购”交易软件,早已偏离了正常的实物商品买卖,演变为高杠杆、高手...
斗金订购APP贵金属期货交易骗...   斗金订购APP的投资者往往被铺天盖地的广告宣传所诱导——注册即送现金、充值返现券等看似诱人的福利...
警惕!云启购APP违规经营,披...   "5元起投、充值返现、稳赚不赔"——这些诱人的广告语背后,可能是一个精心设计的金融陷阱。  近期...
量子淘金APP贵金属投资不合规...   "4元起投""充值返券""月入过万"——这些极具诱惑力的广告语在短视频平台、社交软件上铺天盖地。...
鑫汇智投APP贵金属商品订购骗...   鑫汇智投APP非法期货投资骗局,受害者在投资过程中根本不懂交易,也不去研究和验证这个平台是否正规...
矅光智策APP平台违规经营非法...   矅光智策APP对外宣称是“商城订购”“现货交易”,但查询其经营范围,并未显示具备开展现货期货交易...
黄金珠宝怎么选?内侧刻字都代表... 潮新闻客户端 记者 马焱 通讯员 任倩影 9月8日下午,“云游杭州 乐购金秋”——“浙城·浙放心”活...
原创 冰... 最近全球国债市场正在上演魔幻一幕: 美债、日债、欧债遭遇猛烈抛售,收益率冲到几十年新高,美国财政部都...
日韩股市开盘表现分化 上证报中国证券网讯 9月9日,日韩股市开盘表现分化,韩国综合指数开盘上涨0.26%,报6972.87...