解析、迭代和生成系列文章:https://www.cnblogs.com/f-ck-need-u/p/9832640.html


何为生成器

生成器的wiki页:https://en.wikipedia.org/wiki/Generator_(computer_programming)

在计算机科学中,生成器是特定的迭代器,它完全实现了迭代器接口,所以所有生成器都是迭代器。不过,迭代器用于从数据集中取出元素;而生成器用于"凭空"生成(yield)元素。它不会一次性将所有元素全部生成,而是按需一个一个地生成,所以从头到尾都只需占用一个元素的内存空间。

很典型的一个例子是斐波纳契数列:斐波纳契数列中的数有无穷个,在一个数据结构里放不下,但是可以在需要下一个元素的时候临时计算。

再比如内置函数range()也返回一个类似生成器的对象,每次需要range里的一个数据时才会临时去产生它。如果一定要让range()函数返回列表,必须明确指明list(range(100))

在Python中生成器是一个函数,但它的行为像是一个迭代器。另外,Python也支持生成器表达式。

初探生成器

下面是一个非常简单的生成器示例:

>>> def my_generator(chars):
... for i in chars:
... yield i * 2 >>> for i in my_generator("abcdef"):
... print(i, end=" ") aa bb cc dd ee ff

这里的my_generator是生成器函数(使用了yield关键字的函数,将被声明为generator对象),但是它在for循环中充当的是一个可迭代对象。实际上它本身就是一个可迭代对象:

>>> E = my_generator("abcde")
>>> hasattr(E, "__iter__")
True
>>> hasattr(E, "__next__")
True >>> E is iter(E)
True

由于生成器自动实现了__iter____next__,且__iter__返回的是迭代器自身,所以生成器是一个单迭代器,不支持多迭代

此外,生成器函数中使用for来迭代chars变量,但对于chars中被迭代的元素没有其它操作,而是使用yield来返回这个元素,就像return语句一样。

只不过yield和return是有区别的,yield在生成一个元素后,会记住迭代的位置并将当前的状态挂起(还记住了其它一些必要的东西),等到下一次需要元素的时候再从这里继续yield一个元素,直到所有的元素都被yield完(也可能永远yield不完)。return则是直接退出函数,

yield from

当yield的来源为一个for循环,那么可以改写成yield from。也就是说,for i in g:yield i等价于yield from g

例如下面是等价的。

def mygen(chars):
yield from chars def mygen(chars):
for i in chars:
yiled i

yield from更多地用于子生成器的委托,本文暂不对此展开描述。

生成器和直接构造结果集的区别

下面是直接构造出列表的方式,它和前面示例的生成器结果一样,但是内部工作方式是不一样的。

def mydef(chars):
res = []
for i in chars:
res.append(i * 2)
return res for i in mydef("abcde"):
print(i,end=" ")

这样的结果也能使用列表解析或者map来实现,例如:

for x in [s * 2 for s in "abcde"]: print(x, end=" ")

for x in map( (lambda s: s * 2), "abcde" ): print(x, end=" ")

虽然结果上都相同,但是内存使用上和效率上都有区别。直接构造结果集将会等待所有结果都计算完成后一次性返回,可能会占用大量内存并出现结果集等待的现象。而使用生成器的方式,从头到尾都只占用一个元素的内存空间,且无需等待所有元素都计算完成后再返回,所以将时间资源分布到了每个结果的返回上。

例如总共可能会产生10亿个元素,但只想取前10个元素,如果直接构造结果集将占用巨量内存且等待很长时间,但使用生成器的方式,这10个元素根本不需等待,很快就计算出来。

必须理解的生成器函数:yield如何工作

理解这个工作过程非常重要,是理解和掌握yield的关键。

1.调用生成器函数的时候并没有运行函数体中的代码,它仅仅只是返回一个生成器对象

正如下面的示例,并非输出任何内容,说明没有执行生成器函数体。

def my_generator(chars):
print("before")
for i in chars:
yield i
print("after") >>> c = my_generator("abcd")
>>> c
<generator object my_generator at 0x000001DC167392A0>
>>> I = iter(c)

2.只有开始迭代的时候,才真正开始执行函数体。且在yield之前的代码体只执行一次,在yield之后的代码体只在当前yield结束的时候才执行

>>> next(I)
before # 第一次迭代
'a'
>>> next(I)
'b'
>>> next(I)
'c'
>>> next(I)
'd'
>>> next(I)
after # 最后一次迭代,抛出异常停止迭代
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

一个生成器函数可以有多个yield语句,看看下面的执行过程:

def mygen():
print("1st")
yield 1
print("2nd")
yield 2
print("3rd")
yield 3
print("end") >>> m = mygen()
>>> next(m)
1st
1
>>> next(m)
2nd
2
>>> next(m)
3rd
3
>>> next(m)
end
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

到此,想必已经理解了yield的工作过程。但还有一些细节有必要解释清楚。

yield是一个表达式,但它是有返回值的。需要注意的是,yield操作会在产生并发送了值之后立即让函数处于挂起状态,挂起的时候连返回值都还没来得及返回。所以,yield表达式的返回值是在下一次迭代时才生成返回值的。关于yield的返回值相关,见下面的生成器的send()方法。

yield的返回值和send()

上面说了,yield有返回值,且其返回值是在下一次迭代的时候才返回的。它的返回值根据恢复yield的方式不同而不同

yield有以下几种常见的表达式组合方式:

yield 10            # (1) 丢弃yield的返回值
x = yield 10 # (2) 将yield返回值赋值给x
x = (yield 10) # (3) 等价于 (2)
x = (yield 10) + 11 # (4) 将yield返回值加上11后赋值给x

不管yield表达式的编码方式如何,它的返回值都和调用next()(或__next__())还是生成器对象的send()方法有关。这里的send()方法和next()都用于恢复当前挂起的yield。

如果是调用next()来恢复yield,那么yield的返回值为None,如果调用gen.send(XXX)来恢复yield,那么yield的返回值为XXX。其实next()可以看作是等价于gen.send(None)

再次提醒,yield表达式会在产生一个值后立即挂起,它连返回值都是在下一次才返回的,更不用说yield的赋值和yield的加法操作。

所以,上面的4种yield表达式方式中,如果使用next()来恢复yield,则它们的值分别为:

yield 10       # 先产生10发送出去,然后返回None,但丢弃
x = yield 10 # 返回None,赋值给x
x = (yield 10) # 与上等价
x = (yield 10)+11 # 返回None,整个过程报错,因为None和int不能相加

如果使用的是send(100),上面的4种yield表达式方式中的值分别为:

yield 10       # 先产生10发送出去,然后返回100,但丢弃
x = yield 10 # 返回100,赋值给x,x=100
x = (yield 10) # 与上等价
x = (yield 10)+11 # 返回100,加上11后赋值给x,x=111

为了解释清楚yield工作时的返回值问题,我将用两个示例详细地解释每一次next()/send()的过程。

解释yield的第一个示例

这个示例比较简单。

def mygen():
x = yield 111 # (1)
print("x:", x) # (2)
for i in range(5): # (3)
y = yield i # (4)
print("y:", y) # (5) M = mygen()

1.首先执行下面的代码

>>> print("first:",next(M))
111

这一行执行后,首先将yield出来的111传递给调用者,然后立即在(1)处进行挂起,这时yield表达式还没有进入返回值状态,所以x还未进行赋值操作。但是next(M)已经返回了,所以print正常输出。

无论是next()(或__next__)还是send()都可以用来恢复挂起的yield,但第一次进入yield必须使用next()或者使用send(None)来产生一个挂起的yield。假如第一次就使用send(100),由于此时还没有挂起的yield,所以没有yield需要返回值,这会报错。

2.再执行下面的代码

>>> print("second:",M.send(10))
x: 10
second: 0

这里的M.send(10)首先恢复(1)处挂起的yield,并将10作为该yield的返回值,所以x = 10,然后生成器函数的代码体继续向下执行,到了print("x:",x)正常输出。

再继续进入到for循环迭代中,又再次遇到了yield,于是yield产生range(5)的第一个数值0传递给调用者然后立即挂起,于是M.send()等待到了这个yield值,于是输出"second: 0"。但注意,这时候y还没有进行赋值,因为yield还没有进入返回值的过程。

3.再执行下面的代码

>>> print("third:",M.send(11))
y: 11
third: 1

这里的M.send(11)首先恢复上次挂起的yield并将11作为该挂起yield的返回值,所以y=11,因为yield已经恢复,所以代码体继续详细执行print("y:",y),执行之后进入下一轮for迭代,于是再次遇到yield,它生成第二个range的值1并传递给调用者,然后挂起,于是M.send()接收到数值1并返回,于是输出third: 1。注意,此时的y仍然是11,因为for的第二轮yield还没有返回。

4.继续执行,但使用next()

>>> print("fourth:",next(M))
y: None
fourth: 2

这里的next(M)恢复前面挂起的yield,并且将None作为yield的返回值,所以y赋值为None。然后进入下一轮for循环、遇到yield,next()接收yield出来的值2并返回。

next()可以看作等价于M.send(None)

5.依此类推,直到迭代结束抛出异常

>>> print("fifth:",M.send(13))
y: 13
fifth: 3
>>> print("sixth:",M.send(14))
y: 14
sixth: 4
>>> print("seventh:",M.send(15)) # 看此行
y: 15
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

当发送M.send(15)时,前面挂起的yield恢复并以15作为返回值,所以y=15。于是继续执行,但此时for迭代已经完成了,于是抛出异常,整个生成器函数终止。

解释yield的第二个示例

这个示例稍微复杂些,但理解了前面的yield示例,这个示例也很容易理解。注意,下面的代码不要在交互式python环境中执行,而是以py脚本的方式执行。

def gen():
for i in range(5):
X = int((yield i) or 0) + 10 + i
print("X:",X) G = gen()
for a in G:
print(a)
G.send(77)

执行结果为:

0
X: 87
X: 11
2
X: 89
X: 13
4
X: 91
Traceback (most recent call last):
File "g:\pycode\lists.py", line 10, in <module>
G.send(77)
StopIteration

这里for a in G用的是next(),在这个for循环里又用了G.send(),因为send()接收的值在空上下文,所以被丢弃,但它却将生成器向前移动了一步。

更多的细节请自行思考,如不理解可参考上一个示例的分析。

生成器表达式和列表解析

列表解析/字典解析/集合解析是使用中括号、大括号包围for表达式的,而生成器表达式则是使用小括号包围for表达式,它们的for表达式写法完全一样。

# 列表解析
>>> [ x * 2 for x in range(5) ]
[0, 2, 4, 6, 8] # 生成器表达式
>>> ( x * 2 for x in range(5) )
<generator object <genexpr> at 0x0000013F550A92A0>

在结果上,列表解析等价于list()函数内放生成器表达式:

>>> [ x * 2 for x in range(5) ]
[0, 2, 4, 6, 8] >>> list( x * 2 for x in range(5) )
[0, 2, 4, 6, 8]

但是工作方式完全不一样。列表解析等待所有元素都计算完成后一次性返回,而生成器表达式则是返回一个生成器对象,然后一个一个地生成并构建成列表。生成器表达式可以看作是列表解析的内存优化操作,但执行速度上可能要稍慢于列表解析。所以生成器表达式和列表解析之间,在结果集非常大的时候可以考虑采用生成器表达式。

一般来说,如果生成器表达式作为函数的参数,只要该函数没有其它参数都可以省略生成器表达式的括号,如果有其它参数,则需要括号包围避免歧义。例如:

sum( x ** 2 for x in range(4))

sorted( x ** 2 for x in range(4))

sorted((x ** 2 for x in range(4)),reverse=True)

生成器表达式和生成器函数

生成器表达式一般用来写较为简单的生成器对象,生成器函数代码可能稍多一点,但可以实现逻辑更为复杂的生成器对象。它们的关系就像列表解析和普通的for循环一样。

例如,将字母重复4次的生成器对象,可以写成下面两种格式:

# 生成器表达式
t1 = ( x * 4 for x in "hello" ) # 生成器函数
def time4(chars):
for x in chars:
yield x * 4 t2 = time4("abcd")

使用生成器模拟map函数

map()函数的用法:

map(func, *iterables) --> map object

要想模拟map函数,先看看map()对应的for模拟方式:

def mymap(func,*seqs):
res = []
for args in zip(*args):
res.append( func(*args) )
return res print( mymap(pow, [1,2,3], [2,3,4,5]) )

对此,可以编写出更精简的列表解析方式的map()模拟代码:

def mymap(func, *seqs):
return [ func(*args) for args in zip(*seqs) ] print( mymap(pow, [1,2,3], [2,3,4,5]) )

如果要用生成器来模拟这个map函数,可以参考如下代码:

# 生成器函数方式
def mymap(func, *seqs):
res = []
for args in zip(*args):
yield func(*args) # 或者生成器表达式方式
def mymap(func, *seqs):
return ( func(*args) for args in zip(*seqs) )

Python迭代和解析(5):搞懂生成器和yield机制的更多相关文章

  1. 彻底搞懂 JS 中 this 机制

    彻底搞懂 JS 中 this 机制 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 目录 this 是什么 this 的四种绑定规 ...

  2. Python迭代和解析(1):列表解析

    解析.迭代和生成系列文章:https://www.cnblogs.com/f-ck-need-u/p/9832640.html Python中的解析 Python支持各种解析(comprehensio ...

  3. Python迭代和解析(2):迭代初探

    解析.迭代和生成系列文章:https://www.cnblogs.com/f-ck-need-u/p/9832640.html 在Python中支持两种循环格式:while和for.这两种循环的类型不 ...

  4. python迭代和解析(3):range、map、zip、filter和reduce函数

    解析.迭代和生成系列文章:https://www.cnblogs.com/f-ck-need-u/p/9832640.html range range()是一个内置函数,它返回一个数字序列,功能和Li ...

  5. Python小世界:彻底搞懂Python一切皆对象!!!

    前言 犹记得当初学习Python的时候,对于Python一切皆对象很是懵逼,因为Python是面向对象的动态型语言,而在函数及高阶函数的应用中,如若对于一切皆对象不是有很透彻的了解,基础不是那么牢固的 ...

  6. Python迭代和解析(4):自定义迭代器

    解析.迭代和生成系列文章:https://www.cnblogs.com/f-ck-need-u/p/9832640.html 本文介绍如何自定义迭代器,涉及到类的运算符重载,包括__getitem_ ...

  7. 一文搞懂jsBridge的运行机制

    我司的APP是一个典型的混合开发APP,内嵌的都是前端页面,前端页面要做到和原生的效果相似,就避免不了调用一些原生的方法,jsBridge就是js和原生通信的桥梁,本文不讲概念性的东西,而是通过分析一 ...

  8. 搞懂 XML 解析,徒手造 WEB 框架

    恕我斗胆直言,对开源的 WEB 框架了解多少,有没有尝试写过框架呢?XML 的解析方式有哪些?能答出来吗?! 心中没有答案也没关系,因为通过今天的分享,能让你轻松 get 如下几点,绝对收获满满. a ...

  9. 一文搞懂Python可迭代、迭代器和生成器的概念

    关于我 一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android.Python.Java和Go,这个也是我们团队的主要技术栈. Github:https:/ ...

随机推荐

  1. 马昕璐/唐月晨 《面向对象程序设计(java)》第十一周学习总结

    一:理论部分. 一般将数据结构分为两大类:线性数据结构和非线性数据结构 线性数据结构:线性表.栈.队列.串.数组和文件 非线性数据结构:树和图. 线性表:1.所有数据元素在同一个线性表中必须是相同的数 ...

  2. this全解js

    转(掘金) this关键字是JavaScript中最复杂的机制之一,是一个特别的关键字,被自动定义在所有函数的作用域中,但是相信很多JavaScript开发者并不是非常清楚它究竟指向的是什么.听说你很 ...

  3. elasticsearch 占CPU过高

    一.线上有一台服务器cpu一直跑满,最终定位导是elasticsearch导致的 二.通过一波查找更改jvm和删除 修改后没有生效笔记尴尬 然后网友说删除索引试了试就可以了  哈哈 curl http ...

  4. 关于使用freemarker导出文档的使用

    7.FreeMarker导出word文件,模板:template.ftl/** * 为word加载数据插值 * * @throws IOException */ public void exportW ...

  5. HTML5 history.pushState()和history.replaceState()新增、修改历史记录用法介绍

    抽空研究了下这两个新方法,确实可以解决很多问题 1.使用pushState()方法 可以控制浏览器自带的返回按钮: 有时候我们想让用户点击浏览器返回按钮时,不返回,或执行其他操作,这时,就用到hist ...

  6. Jenkins构建集成部署

    一.可运行Jar配置 1. 设置JDK 2. 设置源码 设置构建脚本 #!/bin/bash export BUILD_ID=xxxxxx_content_170 myPath="/data ...

  7. Java高并发缓存架构,缓存雪崩、缓存穿透之谜

    面试题 了解什么是 redis 的雪崩.穿透和击穿?redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 redis 的穿透? 面试官心理分析 其实这是问到缓存必问的,因为缓存雪崩和穿透,是 ...

  8. PowerShell 中 RunspacePool 执行异步多线程任务

    在 PowerShell 中要执行任务脚本,现在通常使用 Runspace,效率很高:任务比较多时,用 Runspace pool 来执行异步操作,可以控制资源池数量,就像 C# 中的线程池一样 == ...

  9. awk小例子_2_数值统计脚本

    通信公司工作,经常处理各种协议接口,在统计协议接口字段内容时,需要统计字段填写的内容是否正确,和占比是多少.要是单次统计,估计会把人累死,写个脚本统计,轻松便捷. 举例:接口内容 这是一条话单,这样的 ...

  10. JavaScript02-js使用

    JS的用法有两种: 第一种是在html页面通过引入外部js文件,第二种是直接将js代码写在html中.小例如下: 第一种 <script type="text/javascript&q ...