Python Yield Generator详解

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

Python Yield Generator详解

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

generator基础

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

  1. def gen_generator(): 
  2.  
  3.     yield 1 
  4.  
  5.   
  6.  
  7. def gen_value(): 
  8.  
  9.     return 1 
  10.  
  11.      
  12.  
  13. if __name__ == '__main__'
  14.  
  15.     ret = gen_generator() 
  16.  
  17.     print ret, type(ret)    #<generator object gen_generator at 0x02645648> <type 'generator'
  18.  
  19.     ret = gen_value() 
  20.  
  21.     print ret, type(ret)    # 1 <type 'int' 

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

  • 遵循迭代器(iterator)协议,迭代器协议需要实现__iter__、next接口
  • 能过多次进入、多次返回,能够暂停函数体中代码的执行

下面看一下测试代码:

  1. >>> def gen_example(): 
  2.  
  3. ...     print 'before any yield' 
  4.  
  5. ...     yield 'first yield' 
  6.  
  7. ...     print 'between yields' 
  8.  
  9. ...     yield 'second yield' 
  10.  
  11. ...     print 'no yield anymore' 
  12.  
  13. ... 
  14.  
  15. >>> gen = gen_example() 
  16.  
  17. >>> gen.next()    # ***次调用next 
  18.  
  19. before any yield 
  20.  
  21. 'first yield' 
  22.  
  23. >>> gen.next()    # 第二次调用next 
  24.  
  25. between yields 
  26.  
  27. 'second yield' 
  28.  
  29. >>> gen.next()    # 第三次调用next 
  30.  
  31. no yield anymore 
  32.  
  33. Traceback (most recent call last): 
  34.  
  35.   File "<stdin>", line 1, in <module> 
  36.  
  37. StopIteratio  

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

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

  1. def generator_example(): 
  2.  
  3.     yield 1 
  4.  
  5.     yield 2 
  6.  
  7.   
  8.  
  9. if __name__ == '__main__'
  10.  
  11.     for e in generator_example(): 
  12.  
  13.         print e 
  14.  
  15.         # 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:

  1. >>> gen = (x * x for x in xrange(5)) 
  2.  
  3. >>> print gen 
  4.  
  5. <generator object <genexpr> at 0x02655710>  

generator应用

generator基础应用

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

  1. RANGE_NUM = 100 
  2.  
  3.     for i in [x*x for x in range(RANGE_NUM)]: # ***种方法:对列表进行迭代 
  4.  
  5.         # do sth for example 
  6.  
  7.         print i 
  8.  
  9.   
  10.  
  11.     for i in (x*x for x in range(RANGE_NUM)): # 第二种方法:对generator进行迭代 
  12.  
  13.         # do sth for example 
  14.  
  15.         print i  

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

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

  1. def fib(): 
  2.  
  3.     a, b = 1, 1 
  4.  
  5.     while True
  6.  
  7.         yield a 
  8.  
  9.         a, b = b, a+b  

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

generator高级应用

使用场景一:

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

  1. def gen_data_from_file(file_name): 
  2.  
  3.     for line in file(file_name): 
  4.  
  5.         yield line 
  6.  
  7.   
  8.  
  9. def gen_words(line): 
  10.  
  11.     for word in (w for w in line.split() if w.strip()): 
  12.  
  13.         yield word 
  14.  
  15.   
  16.  
  17. def count_words(file_name): 
  18.  
  19.     word_map = {} 
  20.  
  21.     for line in gen_data_from_file(file_name): 
  22.  
  23.         for word in gen_words(line): 
  24.  
  25.             if word not in word_map: 
  26.  
  27.                 word_map[word] = 0 
  28.  
  29.             word_map[word] += 1 
  30.  
  31.     return word_map 
  32.  
  33.   
  34.  
  35. def count_total_chars(file_name): 
  36.  
  37.     total = 0 
  38.  
  39.     for line in gen_data_from_file(file_name): 
  40.  
  41.         total += len(line) 
  42.  
  43.     return total 
  44.  
  45.      
  46.  
  47. if __name__ == '__main__'
  48.  
  49.     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)的方式。下面举一个简单的例子:

  1. def do(a): 
  2.  
  3.      print 'do', a 
  4.  
  5.      CallBackMgr.callback(5, lambda a = a: post_do(a)) 
  6.  
  7. def post_do(a): 
  8.  
  9.     print 'post_do', a  

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

  1. @yield_dec 
  2.  
  3. def do(a): 
  4.  
  5.      print 'do', a 
  6.  
  7.      yield 5 
  8.  
  9.      print 'post_do', a  

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

  1. # -*- coding:utf-8 -*- 
  2.  
  3. import sys 
  4.  
  5. # import Timer 
  6.  
  7. import types 
  8.  
  9. import time 
  10.  
  11.   
  12.  
  13. class YieldManager(object): 
  14.  
  15.     def __init__(self, tick_delta = 0.01): 
  16.  
  17.         self.generator_dict = {} 
  18.  
  19.         # self._tick_timer = Timer.addRepeatTimer(tick_delta, lambda: self.tick()) 
  20.  
  21.   
  22.  
  23.     def tick(self): 
  24.  
  25.         cur = time.time() 
  26.  
  27.         for gene, t in self.generator_dict.items(): 
  28.  
  29.             if cur >= t: 
  30.  
  31.                 self._do_resume_genetator(gene,cur) 
  32.  
  33.   
  34.  
  35.     def _do_resume_genetator(self,gene, cur ): 
  36.  
  37.         try: 
  38.  
  39.             self.on_generator_excute(gene, cur) 
  40.  
  41.         except StopIteration,e: 
  42.  
  43.             self.remove_generator(gene) 
  44.  
  45.         except Exception, e: 
  46.  
  47.             print 'unexcepet error', type(e) 
  48.  
  49.             self.remove_generator(gene) 
  50.  
  51.   
  52.  
  53.     def add_generator(self, gen, deadline): 
  54.  
  55.         self.generator_dict[gen] = deadline 
  56.  
  57.   
  58.  
  59.     def remove_generator(self, gene): 
  60.  
  61.         del self.generator_dict[gene] 
  62.  
  63.   
  64.  
  65.     def on_generator_excute(self, gen, cur_time = None): 
  66.  
  67.         t = gen.next() 
  68.  
  69.         cur_time = cur_time or time.time() 
  70.  
  71.         self.add_generator(gen, t + cur_time) 
  72.  
  73.   
  74.  
  75. g_yield_mgr = YieldManager() 
  76.  
  77.   
  78.  
  79. def yield_dec(func): 
  80.  
  81.     def _inner_func(*args, **kwargs): 
  82.  
  83.         gen = func(*args, **kwargs) 
  84.  
  85.         if type(gen) is types.GeneratorType: 
  86.  
  87.             g_yield_mgr.on_generator_excute(gen) 
  88.  
  89.   
  90.  
  91.         return gen 
  92.  
  93.     return _inner_func 
  94.  
  95.   
  96.  
  97. @yield_dec 
  98.  
  99. def do(a): 
  100.  
  101.     print 'do', a 
  102.  
  103.     yield 2.5 
  104.  
  105.     print 'post_do', a 
  106.  
  107.     yield 3 
  108.  
  109.     print 'post_do again', a 
  110.  
  111.   
  112.  
  113. if __name__ == '__main__'
  114.  
  115.     do(1) 
  116.  
  117.     for i in range(1, 10): 
  118.  
  119.         print 'simulate a timer, %s seconds passed' % i 
  120.  
  121.         time.sleep(1) 
  122.  
  123.         g_yield_mgr.tick()  

注意事项:

(1)Yield是不能嵌套的!

  1. def visit(data): 
  2.  
  3.     for elem in data: 
  4.  
  5.         if isinstance(elem, tuple) or isinstance(elem, list): 
  6.  
  7.             visit(elem) # here value retuened is generator 
  8.  
  9.         else
  10.  
  11.             yield elem 
  12.  
  13.              
  14.  
  15. if __name__ == '__main__'
  16.  
  17.     for e in visit([1, 2, (3, 4), 5]): 
  18.  
  19.         print e  

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

  1. def visit(data): 
  2.  
  3.     for elem in data: 
  4.  
  5.         if isinstance(elem, tuple) or isinstance(elem, list): 
  6.  
  7.             for e in visit(elem): 
  8.  
  9.                 yield e 
  10.  
  11.         else
  12.  
  13.             yield elem  

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

  1. def visit(data): 
  2.  
  3.      for elem in data: 
  4.  
  5.          if isinstance(elem, tuple) or isinstance(elem, list): 
  6.  
  7.              yield from visit(elem) 
  8.  
  9.          else
  10.  
  11.              yield elem  

(2)generator function中使用return

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

  1. def gen_with_return(range_num): 
  2.  
  3.     if range_num < 0: 
  4.  
  5.         return 
  6.  
  7.     else
  8.  
  9.         for i in xrange(range_num): 
  10.  
  11.             yield i 
  12.  
  13.   
  14.  
  15. if __name__ == '__main__'
  16.  
  17.     print list(gen_with_return(-1)) 
  18.  
  19.     print list(gen_with_return(1))  

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

  1. def gen_with_return(range_num): 
  2.  
  3.      if range_num < 0: 
  4.  
  5.          return 0 
  6.  
  7.      else
  8.  
  9.          for i in xrange(range_num): 
  10.  
  11.              yield i  

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

参考

  • http://www.dabeaz.com/generators-uk/
  • https://www.python.org/dev/peps/pep-0380/
  • http://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do
  • http://stackoverflow.com/questions/15809296/python-syntaxerror-return-with-argument-inside-generator 
责任编辑:庞桂玉 来源: Python开发者
相关推荐

2023-12-25 14:50:39

Python迭代器

2013-01-30 10:12:14

Pythonyield

2012-11-23 14:25:10

IBMdW

2021-03-15 12:23:24

Pythonyield代码

2010-03-04 13:37:20

Python yiel

2021-05-13 09:11:11

PythonGo编程

2020-10-25 20:05:29

Pythonyield开发

2023-12-11 13:59:00

YieldPython生成器函数

2022-03-03 08:30:41

GeneratorES6函数

2024-12-13 08:02:10

PythonGenerator懒加载

2021-04-22 21:15:38

Generator函数生成器

2024-11-19 13:20:55

2009-06-29 08:59:05

hbm的generat

2009-06-29 08:58:06

Hibernate的g

2019-08-29 09:11:38

Pythonyield语法

2009-07-02 09:32:47

generator子元Hibernate

2009-12-18 11:37:54

Ruby关键字yiel

2024-03-01 19:35:54

Mybatis开发

2010-03-17 18:38:53

Java编程语言

2022-10-27 13:58:32

Python编程生成器
点赞
收藏

51CTO技术栈公众号