解析、迭代和生成系列文章: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) )

最新文章

  1. Python Day14
  2. nodeJS 简单的模块。
  3. 简单回顾NPOI导入导出excel文件
  4. 自学 Java 怎么入门
  5. HTML学习笔记——块级标签、行级标签、图片标签
  6. idea 工程添加svn关联
  7. highcharts笔记 highcharts学习 highcharts用法
  8. 简单方便的在线客服展示插件 jQuery.onServ
  9. 【HBase学习】Apache HBase 参考手册 中文版
  10. 【转】Cocos2d-x 2.x CCSprite 灰白图的生成(利用shader设置)&mdash;&mdash;2013-08-27 21
  11. 【HDOJ】1969 Pie
  12. Nginx系列~负载均衡服务器与WWW服务器的实现
  13. IOS 新消息通知提示-声音、震动
  14. 浙大pat 1048 题解
  15. Android Studio上修改项目(module)的包名(Package Name)
  16. 五分钟学习React(四):什么是JSX
  17. java学习笔记--从c/c++到java转变
  18. AngularJS DI(依赖注入)实现推测
  19. 关于BDD100k数据输入处理mask变为56*56
  20. WordConut

热门文章

  1. Linux结束进程到底有多少种方法?
  2. 如何在浏览器中输入(myeclipse创建的项目的)地址访问JSP页面
  3. centos7.5安装python3.7
  4. window10 Docker仓库访问
  5. CABaRet: Leveraging Recommendation Systems for Mobile Edge Caching
  6. 微服务架构-选择Spring Cloud,放弃Dubbo
  7. Batch入门教程丨第一章:部署与Hello World!(上)
  8. JavaScript基础系列
  9. [Swift]LeetCode337. 打家劫舍 III | House Robber III
  10. 在 ns-3.25中添加 plc(电力线载波) 模块