由浅入深讲解python中的yield与generator


Posted in Python onApril 05, 2017

前言

本文将由浅入深详细介绍yield以及generator,包括以下内容:什么generator,生成generator的方法,generator的特点,generator基础及高级应用场景,generator使用中的注意事项。本文不包括enhanced generator即pep342相关内容,这部分内容在之后介绍。

generator基础

在python的函数(function)定义中,只要出现了yield表达式(Yield expression),那么事实上定义的是一个generator function, 调用这个generator function返回值是一个generator。这根普通的函数调用有所区别,For example:

def gen_generator():
 yield 1

def gen_value():
 return 1
 
if __name__ == '__main__':
 ret = gen_generator()
 print ret, type(ret) #<generator object gen_generator at 0x02645648> <type 'generator'>
 ret = gen_value()
 print ret, type(ret) # 1 <type 'int'>

从上面的代码可以看出,gen_generator函数返回的是一个generator实例

generator有以下特别:

     •遵循迭代器(iterator)协议,迭代器协议需要实现__iter__ 、next接口

     •能过多次进入、多次返回,能够暂停函数体中代码的执行

下面看一下测试代码: 

>>> def gen_example():
... print 'before any yield'
... yield 'first yield'
... print 'between yields'
... yield 'second yield'
... print 'no yield anymore'
... 
>>> gen = gen_example()
>>> gen.next()
# 第一次调用next
before any yield
'first yield'
>>> gen.next()

# 第二次调用next
between yields
'second yield'
>>> gen.next()

# 第三次调用next
no yield anymore
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteratio

调用gen example方法并没有输出任何内容,说明函数体的代码尚未开始执行。当调用generator的next方法,generator会执行到yield 表达式处,返回yield表达式的内容,然后暂停(挂起)在这个地方,所以第一次调用next打印第一句并返回“first yield”。 暂停意味着方法的局部变量,指针信息,运行环境都保存起来,直到下一次调用next方法恢复。第二次调用next之后就暂停在最后一个yield,再次调用next()方法,则会抛出StopIteration异常。 

因为for语句能自动捕获StopIteration异常,所以generator(本质上是任何iterator)较为常用的方法是在循环中使用: 

def generator_example():
 yield 1
 yield 2

if __name__ == '__main__':
 for e in generator_example():
 print e
 # output 1 2

generator function产生的generator与普通的function有什么区别呢

(1)function每次都是从第一行开始运行,而generator从上一次yield开始的地方运行

(2)function调用一次返回一个(一组)值,而generator可以多次返回

(3)function可以被无数次重复调用,而一个generator实例在yield最后一个值 或者return之后就不能继续调用了

在函数中使用Yield,然后调用该函数是生成generator的一种方式。另一种常见的方式是使用generator expression,For example:

>>> gen = (x * x for x in xrange(5))

>>> print gen

<generator object <genexpr> at 0x02655710>

generator应用

generator基础应用

为什么使用generator呢,最重要的原因是可以按需生成并“返回”结果,而不是一次性产生所有的返回值,况且有时候根本就不知道“所有的返回值”。

比如对于下面的代码  

RANGE_NUM = 100
 for i in [x*x for x in range(RANGE_NUM)]: # 第一种方法:对列表进行迭代
 # do sth for example
 print i

 for i in (x*x for x in range(RANGE_NUM)): # 第二种方法:对generator进行迭代
 # do sth for example
 print i

在上面的代码中,两个for语句输出是一样的,代码字面上看来也就是中括号与小括号的区别。但这点区别差异是很大的,第一种方法返回值是一个列表,第二个方法返回的是一个generator对象。随着RANGE_NUM的变大,第一种方法返回的列表也越大,占用的内存也越大;但是对于第二种方法没有任何区别。

我们再来看一个可以“返回”无穷多次的例子:

def fib():
 a, b = 1, 1
 while True:
 yield a
 a, b = b, a+b

这个generator拥有生成无数多“返回值”的能力,使用者可以自己决定什么时候停止迭代

generator高级应用

使用场景一:

Generator可用于产生数据流, generator并不立刻产生返回值,而是等到被需要的时候才会产生返回值,相当于一个主动拉取的过程(pull),比如现在有一个日志文件,每行产生一条记录,对于每一条记录,不同部门的人可能处理方式不同,但是我们可以提供一个公用的、按需生成的数据流。

def gen_data_from_file(file_name):
 for line in file(file_name):
 yield line

def gen_words(line):
 for word in (w for w in line.split() if w.strip()):
 yield word

def count_words(file_name):
 word_map = {}
 for line in gen_data_from_file(file_name):
 for word in gen_words(line):
  if word not in word_map:
  word_map[word] = 0
  word_map[word] += 1
 return word_map

def count_total_chars(file_name):
 total = 0
 for line in gen_data_from_file(file_name):
 total += len(line)
 return total
 
if __name__ == '__main__':
 print count_words('test.txt'), count_total_chars('test.txt')

上面的例子来自08年的PyCon一个讲座。gen_words gen_data_from_file是数据生产者,而count_words count_total_chars是数据的消费者。可以看到,数据只有在需要的时候去拉取的,而不是提前准备好。另外gen_words中 (w for w in line.split() if w.strip()) 也是产生了一个generator

使用场景二:

一些编程场景中,一件事情可能需要执行一部分逻辑,然后等待一段时间、或者等待某个异步的结果、或者等待某个状态,然后继续执行另一部分逻辑。比如微服务架构中,服务A执行了一段逻辑之后,去服务B请求一些数据,然后在服务A上继续执行。或者在游戏编程中,一个技能分成分多段,先执行一部分动作(效果),然后等待一段时间,然后再继续。对于这种需要等待、而又不希望阻塞的情况,我们一般使用回调(callback)的方式。下面举一个简单的例子:

def do(a):
 print 'do', a
 CallBackMgr.callback(5, lambda a = a: post_do(a))
 
 def post_do(a):
 print 'post_do', a

这里的CallBackMgr注册了一个5s后的时间,5s之后再调用lambda函数,可见一段逻辑被分裂到两个函数,而且还需要上下文的传递(如这里的参数a)。我们用yield来修改一下这个例子,yield返回值代表等待的时间。

@yield_dec
 def do(a):
 print 'do', a
 yield 5
 print 'post_do', a

这里需要实现一个YieldManager, 通过yield_dec这个decrator将do这个generator注册到YieldManager,并在5s后调用next方法。Yield版本实现了和回调一样的功能,但是看起来要清晰许多。

下面给出一个简单的实现以供参考:

# -*- coding:utf-8 -*-
import sys
# import Timer
import types
import time

class YieldManager(object):
 def __init__(self, tick_delta = 0.01):
 self.generator_dict = {}
 # self._tick_timer = Timer.addRepeatTimer(tick_delta, lambda: self.tick())

 def tick(self):
 cur = time.time()
 for gene, t in self.generator_dict.items():
  if cur >= t:
  self._do_resume_genetator(gene,cur)

 def _do_resume_genetator(self,gene, cur ):
 try:
  self.on_generator_excute(gene, cur)
 except StopIteration,e:
  self.remove_generator(gene)
 except Exception, e:
  print 'unexcepet error', type(e)
  self.remove_generator(gene)

 def add_generator(self, gen, deadline):
 self.generator_dict[gen] = deadline

 def remove_generator(self, gene):
 del self.generator_dict[gene]

 def on_generator_excute(self, gen, cur_time = None):
 t = gen.next()
 cur_time = cur_time or time.time()
 self.add_generator(gen, t + cur_time)

g_yield_mgr = YieldManager()

def yield_dec(func):
 def _inner_func(*args, **kwargs):
 gen = func(*args, **kwargs)
 if type(gen) is types.GeneratorType:
  g_yield_mgr.on_generator_excute(gen)

 return gen
 return _inner_func

@yield_dec
def do(a):
 print 'do', a
 yield 2.5
 print 'post_do', a
 yield 3
 print 'post_do again', a

if __name__ == '__main__':
 do(1)
 for i in range(1, 10):
 print 'simulate a timer, %s seconds passed' % i
 time.sleep(1)
 g_yield_mgr.tick()

注意事项:

(1)Yield是不能嵌套的!

def visit(data):
 for elem in data:
 if isinstance(elem, tuple) or isinstance(elem, list):
  visit(elem) # here value retuened is generator
 else:
  yield elem
  
if __name__ == '__main__':
 for e in visit([1, 2, (3, 4), 5]):
 print e

上面的代码访问嵌套序列里面的每一个元素,我们期望的输出是1 2 3 4 5,而实际输出是1  2  5 。为什么呢,如注释所示,visit是一个generator function,所以第4行返回的是generator object,而代码也没这个generator实例迭代。那么改改代码,对这个临时的generator 进行迭代就行了。

def visit(data):
 for elem in data:
 if isinstance(elem, tuple) or isinstance(elem, list):
  for e in visit(elem):
  yield e
 else:
  yield elem

或者在python3.3中 可以使用yield from,这个语法是在pep380加入的

def visit(data):
 for elem in data:
  if isinstance(elem, tuple) or isinstance(elem, list):
  yield from visit(elem)
  else:
  yield elem

(2)generator function中使用return

在python doc中,明确提到是可以使用return的,当generator执行到这里的时候抛出StopIteration异常。

def gen_with_return(range_num):
 if range_num < 0:
 return
 else:
 for i in xrange(range_num):
  yield i

if __name__ == '__main__':
 print list(gen_with_return(-1))
 print list(gen_with_return(1))

但是,generator function中的return是不能带任何返回值的

def gen_with_return(range_num):
 if range_num < 0:
  return 0
 else:
  for i in xrange(range_num):
  yield i

上面的代码会报错:SyntaxError: 'return' with argument inside generator

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
用Python从零实现贝叶斯分类器的机器学习的教程
Mar 31 Python
python获取本地计算机名字的方法
Apr 29 Python
python中的全局变量用法分析
Jun 09 Python
Python的Flask框架中集成CKeditor富文本编辑器的教程
Jun 13 Python
python中实现精确的浮点数运算详解
Nov 02 Python
python去掉 unicode 字符串前面的u方法
Oct 21 Python
利用Pyhton中的requests包进行网页访问测试的方法
Dec 26 Python
Python+OpenCV采集本地摄像头的视频
Apr 25 Python
python使用sessions模拟登录淘宝的方式
Aug 16 Python
Python TCP通信客户端服务端代码实例
Nov 21 Python
python标识符命名规范原理解析
Jan 10 Python
Python 如何将integer转化为罗马数(3999以内)
Jun 05 Python
Python中shutil模块的学习笔记教程
Apr 04 #Python
python 遍历字符串(含汉字)实例详解
Apr 04 #Python
python模拟登录并且保持cookie的方法详解
Apr 04 #Python
python 容器总结整理
Apr 04 #Python
详解Python中最难理解的点-装饰器
Apr 03 #Python
JSON Web Tokens的实现原理
Apr 02 #Python
Python 40行代码实现人脸识别功能
Apr 02 #Python
You might like
PHP学习之数组的定义和填充
2011/04/17 PHP
PHP响应post请求上传文件的方法
2015/12/17 PHP
Yii2框架dropDownList下拉菜单用法实例分析
2016/07/18 PHP
php封装的单文件(图片)上传类完整实例
2016/10/18 PHP
PHP实现随机生成水印图片功能
2017/03/22 PHP
php利用ffmpeg提取视频中音频与视频画面的方法详解
2017/06/07 PHP
php实现文章评论系统
2019/02/18 PHP
Locate a File Using a File Open Dialog Box
2007/06/18 Javascript
javascript Math.random()随机数函数
2009/11/04 Javascript
JQuery Ajax通过Handler访问外部XML数据的代码
2010/06/01 Javascript
JS父页面与子页面相互传值方法
2014/03/05 Javascript
jquery的clone方法应用于textarea和select的bug修复
2014/06/26 Javascript
Bootstrap CDN和本地化环境搭建
2016/10/26 Javascript
VueJS全面解析
2016/11/10 Javascript
基于JavaScript实现下拉列表左右移动代码
2017/02/07 Javascript
Angular 4依赖注入学习教程之ValueProvider的使用(七)
2017/06/04 Javascript
vue2 mint-ui loadmore实现下拉刷新,上拉更多功能
2018/03/21 Javascript
vue keep-alive 动态删除组件缓存的例子
2019/11/04 Javascript
vue 组件间的通信之子组件向父组件传值的方式
2020/07/29 Javascript
[03:56]DOTA2完美大师赛趣味视频之小鸽子和Mineski打台球
2017/11/24 DOTA
python基础教程之序列详解
2014/08/29 Python
Python命令行参数解析模块getopt使用实例
2015/04/13 Python
Python numpy实现数组合并实例(vstack,hstack)
2018/01/09 Python
Python 实现平台类游戏添加跳跃功能
2020/03/27 Python
使用keras2.0 将Merge层改为函数式
2020/05/23 Python
python 中的9个实用技巧,助你提高开发效率
2020/08/30 Python
scrapy在python爬虫中搭建出错的解决方法
2020/11/22 Python
Python爬虫定时计划任务的几种常见方法(推荐)
2021/01/15 Python
CSS3实现超酷的黑猫警长首页
2016/04/26 HTML / CSS
HTML 5.1来了 9月份正式发布 更新内容预览
2016/04/26 HTML / CSS
美国专业级皮肤病和spa品质护肤品的高级零售网站:SkinCareRx
2017/02/06 全球购物
写给女朋友的道歉信
2014/01/12 职场文书
大学生活自我评价
2014/04/09 职场文书
实验室安全管理制度
2015/08/05 职场文书
毕业班工作总结
2015/08/10 职场文书
解决tk mapper 通用mapper的bug问题
2021/06/16 Java/Android