什么是Python Generator(生成器) ?
我们提供的服务有:成都做网站、成都网站设计、微信公众号开发、网站优化、网站认证、双塔ssl等。为成百上千家企事业单位解决了网站和推广的问题。提供周到的售前咨询和贴心的售后服务,是有科学管理、有技术的双塔网站制作公司
Python Generator(生成器)用于在内存资源有限的情况下,把处理大数据的任务,分解为一段一段可以管理和处理的数据块(chunk),建立起数据流(data pipeline),从而一步一步的解决完大数据任务的技术。例如,假设有500G的数据待处理,内存只有32G,我们可以把数据分为200M的数据块,然后借助Python Generator技术,实现一边加载数据一边进行数据处理的效果。
生成器关键字yield 与 函数返回语句return的区别 :
return语句 终止函数运行并返回return语句后面的变量值;return语句后面的语句不执行。
Python生成器可以由以下两种方式创建:
对于程序而言,内存也是很重要的,因为程序中很多数据都是保存在内存中的,如果内存中存储的数据过多,那么系统就会崩溃,这是人们不希望发生的。
可以采用生成器推导式来解决内存不足的问题。例如,利用生成器推导式创建一个生成器n,数据为1~33数字,可以写成n = (i for i in range(1, 34))。这样当程序需要一个数时,程序才生成数据,可以节省内存。然而生成器推导式太过简单,只能用一行代码的形式实现,如果要创建复杂的生成器,如创建一个生成器f,生成前10个斐波那契数字,生成器推导式已经不能满足需求了,因为斐波那契数列最开始的两个数都无法赋值。
函数可以实现复杂的功能,然而要节省内存,就需要使用生成器函数。生成器函数与普通函数的区别是函数中包含关键字yield。实际上只要含有yield关键字的函数就是生成器函数。
生成器函数是用函数实现生成器。定义生成器函数的语法格式如下:
def fib(): # 定义生成器函数
... a, b = 1, 1 # 定义初始值
... while True:
... yield a # 暂停执行,返回一个新变量值
... a, b = b, a+b
...
a= fib() # 调用生成器函数
for i in range(10):
... print(next(a)) # 调用生成器函数的yield生成值
...
1
1
2
3
5
8
13
21
34
55
def 函数名(参数):
函数体
yield 变量名
函数体
由语法格式可知,生成器函数与普通函数的区别在于函数体部分,生成器函数的函数体含有“yield 变量名”语句。yield的功能类似于return,return是函数返回值,yield的功能也是返回变量,但是它仅返回变量而不退出函数,因此,yield可以看作是多次返回变量且不会退出函数的return。
在调用生成器函数时,写上函数名与参数,并通过一个变量接收返回值,语法格式如下:
变量名 = 函数名(参数)
调用生成器函数的yield生成值的第一种方法如下:
next(变量名)
第二种方法如下:
变量名.__next__()
在掌握了生成器函数的定义和调用之后,就可以使用生成器函数实现生成前10个斐波那契数字的案例了。这个案例主要分为三步,第一步是定义生成器函数,第二步是调用并赋值,第三步是打印结果,代码如下:
在上述程序中,首先定义生成器函数fib(),函数内先定义斐波那契数列的两个初始值,再写一个while True死循环。这个死循环有些特别,先是用yield生成待使用的数字,再通过赋值语句“a, b = b, a+b”将b的值赋给a,将a+b的值赋给b,每次循环都是如此。然后调用生成器函数fib(),再调用生成器函数的yield生成值,最后打印结果。由于需要生成前10个斐波那契数字,因此可以采用for循环,每循环一次生成并打印一个斐波那契数字,共循环10次。第一次循环时,调用yield生成值a,即1;第二次循环时,调用yield生成值a,a被赋值成b的值,即1,而b被赋值成a+b的值,即2;第三次循环时,调用yield生成值a,a被赋值成b的值,而此时b的值是上次赋值的a+b的值,即2……以此类推,就得到了整个斐波那契数列。
如果函数要返回一系列结果,我们常见的方法就是将结果放到一份列表中,然后返回给调用者。比如下面的函数,返回字符串中每个单词的首字母在真个字符串中的索引:
运行结果:
上述的结果完全符合我们的预期,但 get_word_index 函数不够简洁。下面我们尝试使用生成器来实现:
运行结果:
改写之后,不仅运行结果符合要求,由于不需要和 result 列表交互,函数也变得非常简洁。下面我们就来详细学习下生成器吧~
生成器是指使用 yield 表达式的函数,调用生成器函数时,它并不会真的运行,而是会返回迭代器。每次在这个迭代器上面调用内置的 next 函数时,迭代器就会把生成器推进到下一个 yield 表达式那里。生成器传给 yield 的值均会由迭代器返回给调用者。
此外,如果输入量非常大,使用列表作为返回值,那么程序就有可能耗尽内存并崩溃。相反,使用生成器之后,则可以应对任意长度的输入数据。
例如,下面这个生成器函数可以获取文件中单词的索引,而不管文件内容多大,该函数执行时消耗的内存,只由单行的文本长度决定:
其中 test_generator.txt 中的内容如下:
运行结果:
下面这句话特别重要: 生成器函数返回的迭代器,是由状态的,及调用者不应该反复使用它 。我们那 word_index_iter 来说明:
如果想重复调用,请将其封装成容器:
运行结果:
关于上述自定义容器的实现原理,我的另外一篇文章做了详细介绍,链接奉上:
生成器似乎并不是一个经常被开发者讨论的语法,因此也就没有它的大兄弟迭代器那么著名。大家不讨论它并不是说大家都已经对它熟悉到人尽皆知,与之相反,即使是工作多年的开发者可能对生成器的运行过程还是知之甚少。这是什么原因导致的呢?我猜想大概有以下几点原因: (1)运行流程不同寻常,(2)日常开发不需要,(3)常常将生成器与迭代器混淆。 生成器的运行流程可以按照协程来理解,也就是说 返回中间结果,断点继续运行 。这与我们通常对于程序调用的理解稍有差异。这种运行模式是针对什么样的需求呢? 一般而言,生成器是应用于大量磁盘资源的处理。 比如一个很大的文件,每次读取一行,下一次读取需要以上一次读取的位置为基础。下面就通过代码演示具体看看生成器的运行机制、使用方式以及与迭代器的比较。
什么是生成器?直接用文字描述可能太过抽象,倒不如先运行一段代码,分析这段代码的运行流程,然后总结出自己对生成器的理解。
从以上演示可以看出,这段代码定义了一个函数,这个函数除了yield这个关键字之外与一般函数并没有差异,也就是说生成器的魔法都是这个yield关键字引起的。 第一点,函数的返回值是一个生成器对象。 上述代码中,直接调用这个看似普通的函数,然后将返回值打印出来,发现返回值是一个对象,而并不是普通函数的返回值。 第二点,可以使用next对这个生成器对象进行操作 。生成器对象天然的可以被next函数调用,然后返回在yield关键字后面的内容。 第三,再次调用next函数处理生成器对象,发现是从上次yield语句之后继续运行,直到下一个yield语句返回。
生成器的运行流程确实诡异,下面还要演示一个生成器可以执行的更加诡异的操作:运行过程中向函数传参。
返回生成器和next函数操作生成器已经并不奇怪了,但是在函数运行过程中向其传参还是让人惊呆了。 调用生成器的send函数传入参数,在函数内使用yield语句的返回值接收,然后继续运行直到下一个yield语句返回。 以前实现这种运行流程的方式是在函数中加上一个从控制台获取数据的指令,或者提前将参数传入,但是现在不用了,send方式使得传入的参数可以随着读取到的参数变化而变化。
很多的开发者比较容易混淆生成器和迭代器,而迭代器的运行过程更加符合一般的程序调用运行流程,因此从亲进度和使用熟悉度而言,大家对迭代器更有好感。比如下面演示一个对迭代器使用next方法进行操作。
从以上演示来看,大家或许会认为迭代器比生成器简单易用得太多了。不过,如果你了解迭代器的实现机制,可能就不会这么早下结论了。python内置了一些已经实现了的迭代器使用确实方便,但是如果需要自己去写一个迭代器呢?下面这段代码就带大家见识以下迭代器的实现。
在python中,能被next函数操作的对象一定带有__next__函数的实现,而能够被迭代的对象有必须实现__iter__函数。看了这么一段操作,相信大家对迭代器实现的繁琐也是深有体会了,那么生成器的实现是不是会让你觉得更加简单易用呢?不过千万别产生一个误区,即生成器比迭代器简单就多用生成器。 在实际开发中,如果遇到与大量磁盘文件或者数据库操作相关的倒是可以使用生成器。但是在其他的任务中使用生成器难免有炫技,并且使逻辑不清晰而导致可读性下降的嫌疑。 这大概也能解释生成器受冷落的原因。不过作为一个专业的开发者,熟悉语言特性是分内之事。
到此,关于生成器的讨论就结束了。本文的notebook版本文件在github上的cnbluegeek/notebook仓库中共享,欢迎感兴趣的朋友前往下载。
迭代器
迭代是Python最强大的功能之一,是访问集合元素的一种方式。
迭代器是一个可以记住遍历的位置的对象。
迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束,迭代器只能往前不会后退。
迭代器有两个基本的方法:iter()和next()。
生成器
在Python中,使用了yield的函数被称为生成器。
跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。
在调用生成器运行的过程中,每次遇到yield时函数会暂停并保存当前所有的运行信息,返回yield的值,并在下一次执行next()方法时从当前位置继续运行。
调用一个生成器函数,返回的是一个迭代器对象。
迭代器与生成器之间的区别:
迭代器是一个更抽象的概念,任何对象,如果它的类有NEXTiter方法返回自己本身,对于string、list、dict、tuple等这类容器对象,使用for循环遍历是很方便的。在后台For语言对容器对象条用iter()函数,iter()是Python的内置函数。iter()会返回一个定义了next()方法迭代器对象,在容器中逐个访问容器的元素,next()也是Python的内置函数,next()会抛出StopIteration异常。
生成器是创新迭代器的简单而强大的工具,它们写起来就好像正则函数,只是在需要返回数据的时候使用yield 语句。
迭代器协议,对象需要提供next()方法,它要么返回迭代中的下一项,要么就引起一个StopIteration异常,终止迭代。
可迭代对象,实现了迭代器协议对象。list、tuple、dict都是Iterable可迭代的对象,但不是Iterator迭代器对象。
使用场景:
一般我们在循环迭代的时候,都必须等待循环结束的时候才return结果。
数量小的时候还可以,但是如果循环次数是上百万,上亿的话,要等多久?
如果循环不涉及龟速I/O还行,但是如果涉及I/O阻塞,一个堵几秒,后面几百万几亿个用户等着,要等多久?
解决方法:
所以这里肯定要并行处理。除了传统的多线程多进程外,我们还可以用Generator 生成器,由yield代替return,每次循环都有返回,而不是等所有的执行完再返回。
好处:
如果用return,循环中所有的数据都要累计到内存里,直到结束。这样不友好。
而yield是一次次的返回结果,就不会在内存里累计了。数据量越大,越明显。
生成器概念:
只要在函数中使用了yield关键字,这个函数就会返回一个generator的对象
有了这个对象,我们就能使用一系列的操作来控制循环的结果
比如 next()就是获取下一个结果
yield和generator的关系,简单来说就是一个起因一个结果:只要写上yield, 其所在的函数就立马变成一个generator object对象。
range 和 xrange的区别 :
Python中我们使用range()函数生成数列非常常用。而xrange()的使用方法、效果几乎一模一样,唯一不同的就是——xrange()返回的是生成器,而不是直接的结果。
如果数据量大时,xrange()能极大的减小内存占用,带来卓越的性能提升。
yield 作为暂停键
每次程序遇到yield时就会暂停,当调用next的时候,就会继续
yield from
如果我们需要在一个生成器中使用另一个生成器,可以用yield from 简化