Python中的yield和Generators(生成器)
本文目的
解释yield关键字到底是什么,为什么它是有用的,以及如何来使用它。
协程与子例程
我们调用一个普通的Python函数时,一般是从函数的第一行代码开始执行,结束于return语句、异常或者函数结束(可以看作隐式的返回None)。一旦函数将控制权交还给调用者,就意味着全部结束。函数中做的所有工作以及保存在局部变量中的数据都将丢失。再次调用这个函数时,一切都将从头创建。
对于在计算机编程中所讨论的函数,这是很标准的流程。这样的函数只能返回一个值,不过,怎么才能创建能产生一个序列的函数呢?换句话说,这个函数需要能够“保存自己的工作”。
python中的生成器可以实现这一点。
生成器中,我们的函数并没有像通常意义那样返回。常规函数中的return隐含的意思是函数正将执行代码的控制权返回给函数被调用的地方。而"yield"的隐含意思是控制权的转移是临时和自愿的,我们的函数将来还会收回控制权。
生成器(以及yield语句)最初的引入是为了让程序员可以更简单的编写用来产生值的序列的代码。 以前,要实现类似随机数生成器的东西,需要实现一个类或者一个模块,在生成数据的同时保持对每次调用之间状态的跟踪。引入生成器之后,这变得非常简单。
为了更好的理解生成器所解决的问题,让我们来看一个例子。在了解这个例子的过程中,请始终记住我们需要解决的问题:生成值的序列。
注意:在Python之外,最简单的生成器应该是被称为协程(coroutines)的东西。在本文中,我将使用这个术语。请记住,在Python的概念中提到的协程就是生成器。Python正式的术语是生成器;协程只是便于讨论,在语言层面并没有正式定义。
例子:有趣的素数
写一个函数,输入参数是一个int的list,返回一个可以迭代的包含素数的结果。
初始代码:
- import math
- def get_primes1(input_list):
- result_list = list()
- for element in input_list:
- if is_prime(element):
- result_list.append(element)
- print(result_list)
- return result_list
- # 或者更好一些的...
- def get_primes2(input_list):
- return (element for element in input_list if is_prime(element))
- # 下面是 is_prime 的一种实现...
- def is_prime(number):
- if number < 2:
- print("请提供一个大于等于2的自然数")
- return False
- elif number == 2: # 2为素数
- print("%d是素数" % number)
- return True
- elif number % 2 == 0: #加这一步是为了提高判断效率
- print("%d不是素数" % number)
- return False
- else:
- #for current in range(3, int(math.sqrt(number) + 1), 2):
- for current in range(2, int(math.sqrt(number)+1)): #需要加1,因为要取到sqrt(num)的值,而不是只取到它前面的值。
- if number % current == 0:
- print("%d不是素数"%number)
- return False
- print("%d是素数" % number)
- return True
- #print(is_prime(17))
- input_list=[1, 2, 3, 4, 5, 6, 7, 9, 11, 13, 17, 19]
- get_primes1(input_list)
- get_primes2(input_list)
- # 质数又称素数。一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数
- # 100以内质数表
- # 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
- # 53 59 61 67 71 73 79 83 89 97
- # 质数具有许多独特的性质:
- # (1)质数p的约数只有两个:1和p。
- # (2)初等数学基本定理:任一大于1的自然数,要么本身是质数,要么可以分解为几个质数之积,且这种分解是唯一的。
- # (3)质数的个数是无限的。
- # (4)质数的个数公式π(n)是不减函数。
- # (5)若n为正整数,在n方到(n+1)方之间至少有一个质数。
- # (6)若n为大于或等于2的正整数,在n到n!之间至少有一个质数。
- # (7)若质数p为不超过n(n>=4)的最大质数,则。
- # (8)所有大于10的质数中,个位数只有1,3,7,9。
- # 基本判断思路:
- # 在一般领域,对正整数n,如果用2到sqrt(n)间的所有整数去除,均无法整除,则n为质数。
- # 质数大于等于2 不能被它本身和1以外的数整除
好想我们的问题解决了。但是,真是如此吗?
处理无限序列
设想这样一个情况:若我们的get_primes函数用于处理一个很大的list,这个list大到仅仅是创建它,就会用完系统的所有内存。怎么办呢?
换句话说,在调用get_primes函数时带上一个start参数,返回所有大于这个参数的素数。
我们来看看这个新需求,很明显只是简单的修改get_primes是不可能的。 很显然,我们不可能返回包含从start到无穷的所有的素数的列表 (虽然有很多有用的应用程序可以用来操作无限序列)。
阻止我们编写满足新需求的函数的核心障碍是什么呢?通过思考,我们得到这样的结论:函数只有一次返回结果的机会,因而必须一次返回所有的结果。得出这样的结论似乎毫无意义;“函数不就是这样工作的么”,通常我们都这么认为的。可是,“如果它们并非如此呢?”
想象一下,如果get_primes可以只是简单返回下一个值,而不是一次返回全部的值,我们就不再需要创建列表。没有列表,就没有内存的问题!
走进生成器
这类问题极其常见以至于Python专门加入了一个结构来解决它:生成器。一个生成器会“生成”值。创建一个生成器几乎和生成器函数的原理一样简单。
一个生成器函数的定义很像一个普通的函数,除了当它要生成一个值的时候,使用yield关键字而不是return。如果一个def的主体包含yield,这个函数会自动变成一个生成器(即使它包含一个return)。生成器就是一类特殊的迭代器。作为一个迭代器,生成器必须要定义一些方法(method),其中一个就是__next__()。如同迭代器一样,我们可以使用next()函数来获取下一个值。既然生成器是一个迭代器,它可以被用在for循环中。
每当生成器被调用的时候,它会返回一个值给调用者。在生成器内部使用yield来完成这个动作。为了记住yield到底干了什么,最简单的方法是把它当作专门给生成器函数用的特殊的return。
下面是一个简单的生成器函数:
- >>> def myGen():
print('生成器被调用。。。')
yield 1
yield 2- # 调用方法
>>> myG=myGen()
>>> next(myG)
生成器被调用。。。
1
>>> next(myG)
2
>>> next(myG)- # 调用方法2
>>> for value in myGen():
print(value)
生成器被调用。。。
1
2
>>>
魔法?
那么神奇的部分在哪里?
当一个生成器函数调用yield,生成器函数的“状态”会被冻结,所有的变量的值会被保留下来,下一行要执行的代码的位置也会被记录,直到再次调用next()。一旦next()再次被调用,生成器函数会从它上次离开的地方开始。如果永远不调用next(),yield保存的状态就被无视了。
我们来重写get_primes()函数,这次我们把它写作一个生成器。使用一个简单的while循环,我们创造了自己的无穷串列。
- def get_primes(number):
- while True:
- if is_prime(number):
- yield number
- number += 1
如果生成器函数调用了return,或者执行到函数的末尾,会出现一个StopIteration异常。 这会通知next()的调用者这个生成器没有下一个值了。这也是这个while循环在我们的get_primes()函数出现的原因。这个while循环是用来确保生成器函数永远也不会执行到函数末尾的。只要调用next()这个生成器就会生成一个值。这是一个处理无穷序列的常见方法(这类生成器也是很常见的)。
执行流程
让我们回到调用get_primes的地方:solve_number_10。
- def solve_number_10():
- total = 2
- for next_prime in get_primes(3):
- if next_prime < 2000000:
- total += next_prime
- else:
- print(total)
- return
我们来看一下solve_number_10的for循环中对get_primes的调用,观察一下前几个元素的创建方法有助于我们的理解。当for循环从get_primes请求第一个值时,我们进入get_primes,这时与进入普通函数没有区别。
- 进入第三行的while循环
- 停在if条件判断(3是素数)
- 通过yield将3和执行控制权返回给solve_number_10
接下来,回到insolve_number_10:
- for循环得到返回值3
- for循环将其赋给next_prime
- total加上next_prime
- for循环从get_primes请求下一个值
这次,进入get_primes时并没有从开头执行,我们从第5行继续执行,也就是上次离开的地方。
- def get_primes(number):
- while True:
- if is_prime(number):
- yield number
- number +=1
最关键的是,number还保持我们上次调用yield时的值(例如3)。记住,yield会将值传给next()的调用方,同时还会保存生成器函数的“状态”。接下来,number加到4,回到while循环的开始处,然后继续增加直到得到下一个素数(5)。我们再一次把number的值通过yield返回给solve_number_10的for循环。这个周期会一直执行,直到for循环结束(得到的素数大于2,000,000)。
更给力点
在PEP 342中加入了将值传给生成器的支持。PEP 342加入了新的特性,能让生成器在单一语句中实现,生成一个值(像从前一样),接受一个值,或同时生成一个值并接受一个值。
我们用前面那个关于素数的函数来展示如何将一个值传给生成器。这一次,我们不再简单地生成比某个数大的素数,而是找出比某个数的等比级数大的最小素数(例如10, 我们要生成比10,100,1000,10000 ... 大的最小素数)。我们从get_primes开始:
- def print_successive_primes(iterations, base=10):
- # 像普通函数一样,生成器函数可以接受一个参数
- prime_generator = get_primes(base)
- # 这里以后要加上点什么
- for power in range(iterations):
- # 这里以后要加上点什么
- def get_primes(number):
- while True:
- if is_prime(number):
- # 这里怎么写?
get_primes的后几行需要着重解释。yield关键字返回number的值,而像 other = yield foo 这样的语句的意思是,"返回foo的值,这个值返回给调用者的同时,将other的值也设置为那个值"。你可以通过send方法来将一个值”发送“给生成器。
- def get_primes(number):
- while True:
- if is_prime(number):
- number = yield number
- number += 1
通过这种方式,我们可以在每次执行yield的时候为number设置不同的值。现在我们可以补齐print_successive_primes中缺少的那部分代码:
- def print_successive_primes(iterations, base=10):
- # 像普通函数一样,生成器函数可以接受一个参数
- prime_generator = get_primes(base)
- prime_generator.send(None)
- for power in range(iterations):
- print(prime_generator.send(base ** power))
这里有两点需要注意:首先,我们打印的是generator.send的结果,这是没问题的,因为send在发送数据给生成器的同时还返回生成器通过yield生成的值(就如同生成器中yield语句做的那样)。
第二点,看一下prime_generator.send(None)这一行,当你用send来“启动”一个生成器时(就是从生成器函数的第一行代码执行到第一个yield语句的位置),你必须发送None。这不难理解,根据刚才的描述,生成器还没有走到第一个yield语句,如果我们发生一个真实的值,这时是没有人去“接收”它的。一旦生成器启动了,我们就可以像上面那样发送数据了。
综述
在文章的后半部分,我们来讨论一些yield的高级用法及其效果。yield已经成为Python最强大的关键字之一。现在我们已经对yield是如何工作的有了充分的理解,我们已经有了必要的知识,可以去了解yield的一些更“费解”的应用场景。
不管你信不信,我们其实只是揭开了yield强大能力的一角。例如,send确实如前面说的那样工作,但是在像我们的例子这样,只是生成简单的序列的场景下,send几乎从来不会被用到。下面我贴一段代码,展示send通常的使用方式。对于这段代码如何工作以及为何可以这样工作,在此我并不打算多说,各位同学自己可以找找资料。
- import random
- def get_data():
- """返回0到9之间的3个随机数"""
- return random.sample(range(10), 3)
- def consume():
- """显示每次传入的整数列表的动态平均值"""
- running_sum = 0
- data_items_seen = 0
- while True:
- data = yield
- data_items_seen += len(data)
- running_sum += sum(data)
- print('The running average is {}'.format(running_sum / float(data_items_seen)))
- def produce(consumer):
- """产生序列集合,传递给消费函数(consumer)"""
- while True:
- data = get_data()
- print('Produced {}'.format(data))
- consumer.send(data)
- yield
- if __name__ == '__main__':
- consumer = consume()
- consumer.send(None)
- producer = produce(consumer)
- for _ in range(10):
- print('Producing...')
- next(producer)
我希望您可以从本文的讨论中获得一些关键的思想:
- generator是用来产生一系列值的
- yield则像是generator函数的返回结果
- yield唯一所做的另一件事就是保存一个generator函数的状态
- generator就是一个特殊类型的迭代器(iterator)
- 和迭代器相似,我们可以通过使用next()来从generator中获取下一个值
- 通过隐式地调用next()来忽略一些值
Python中的yield和Generators(生成器)的更多相关文章
- Python中的yield生成器的简单介绍
Python yield 使用浅析(整理自:廖 雪峰, 软件工程师, HP 2012 年 11 月 22 日 ) 初学 Python 的开发者经常会发现很多 Python 函数中用到了 yield 关 ...
- [转]关于Python中的yield
在介绍yield前有必要先说明下Python中的迭代器(iterator)和生成器(constructor). 一.迭代器(iterator) 在Python中,for循环可以用于Python中的任何 ...
- 【转载】关于Python中的yield
在介绍yield前有必要先说明下Python中的迭代器(iterator)和生成器(constructor). 一.迭代器(iterator) 在Python中,for循环可以用于Python中的任何 ...
- 关于Python中的yield
关于Python中的yield 在介绍yield前有必要先说明下Python中的迭代器(iterator)和生成器(constructor). 一.迭代器(iterator) 在Python中,f ...
- 关于Python中的yield(转载)
您可能听说过,带有 yield 的函数在 Python 中被称之为 generator(生成器),何谓 generator ? 我们先抛开 generator,以一个常见的编程题目来展示 yield ...
- 深入理解Python中的yield和send
send方法和next方法唯一的区别是在执行send方法会首先把上一次挂起的yield语句的返回值通过参数设定,从而实现与生成器方法的交互. 但是需要注意,在一个生成器对象没有执行next方法之前,由 ...
- Python中的列表解析和生成器表达式
Python中的列表解析和生成器表达式 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.列表解析案例 #!/usr/bin/env python #_*_coding:utf-8 ...
- python 中的 yield 究竟为何物?生成器和迭代器的区别?
当你突然看到别人的代码中出现了一个好像见过但又没用过的关键词 比如 yield ,你是否会觉得这段代码真是高大上呢? 或许只有我这种小白才会这样子觉得,就在刚刚,我就看见了别人的代码中的yield,觉 ...
- python中的yield
在理解yield之前,要首先明白什么是generator,在理解generator之前首先要理解可迭代的概念. 可迭代(iterables)在你创建一个list的时候,可以逐个读取其中的元素,该逐个读 ...
随机推荐
- 企业私有云部署im,视频服务
1,安全问题 2,员工跨地域 3,内部视频培训 考勤申请,设备借用申请 名片申请 会议室预订 审批 内网,局域网部署 Android源码 https://github.com/starrtc/andr ...
- Check which .NET Framework version is installed
his article will help you to know which .NET Framework version is installed from command line. Check ...
- 很简单的在Ubuntu系统下安装字体和切换默认字体的方法
摘要: Ubuntu系统安装好后,默认字体对于中文的支持看上去不太美丽,于是很多朋友可能需要设置系统的默认字体为自己喜欢的字体.本文主要介绍如何解决这两个问题. 说明:测试系统是Ubuntu14.04 ...
- SSD 固态硬盘,Trim指令 ,查看状态、开启、关闭
一说到SSD 固态硬盘,经常会看到Trim指令这个名词,那什么是Trim? Trim是什么? 为了解决硬盘降速的问题,微软联合各大SSD厂商开发了一个新技术——Trim.Trim指令也叫disab ...
- GRAPH ATTENTION NETWORKS
基本就是第一层concatenate,第二层不concatenate. 相关论文: Semi-Supervised Classification with Graph Convolutional Ne ...
- Oracle 数据泵使用详解
数据泵使用EXPDP和IMPDP时应该注意的事项: EXP和IMP是客户端工具程序,它们既可以在客户端使用,也可以在服务端使用. EXPDP和IMPDP是服务端的工具程序,他们只能在ORACLE服务端 ...
- 针对程序集 'SqlServerTime' 的 ALTER ASSEMBLY 失败,因为程序集 'SqlServerTime' 未获授权(PERMISSION_SET = EXTERNAL_ACCESS)
错误: 针对程序集 'SqlServerTime' 的 ALTER ASSEMBLY 失败,因为程序集 'SqlServerTime' 未获授权(PERMISSION_SET = EXTERNAL_A ...
- 于erlang依赖的linux调优
[皇室]杭州-sunface(61087682) 上午 9:42:02 http://docs.basho.com/riak/latest/ops/tuning/linux/ 这篇文章对于erlang ...
- MySQL查看某库表大小及锁表情况
查询所有数据库占用磁盘空间大小的SQL语句: 语句如下: select TABLE_SCHEMA, concat(truncate(sum(data_length)/1024/1024,2),' MB ...
- log4net 日志配置及使用
一.log4net按照不同的[LEVEL]级别输出到不同文件 <log4net> <!--错误日志:::记录错误日志--> <!--按日期分割日志文件 一天一个--> ...