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:

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)较为常用的方法是在循环中使用: 

1 def generator_example():
2     yield 1
3     yield 2
4 
5 if __name__ == '__main__':
6     for e in generator_example():
7         print e
8         # 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 呢,最重要的原因是可以按需生成并“返回”结果,而不是一次性产生所有的返回值,况且有时候根本就不知道“所有的返回值”。比如对于下面的代码  

1     RANGE_NUM = 100
2     for i in [x*x for x in range(RANGE_NUM)]: # 第一种方法:对列表进行迭代
3         # do sth for example
4         print i
5 
6     for i in (x*x for x in range(RANGE_NUM)): # 第二种方法:对 generator 进行迭代
7         # do sth for example
8         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),比如现在有一个日志文件,每行产生一条记录,对于每一条记录,不同部门的人可能处理方式不同,但是我们可以提供一个公用的、按需生成的数据流。

 1 def gen_data_from_file(file_name):
 2     for line in file(file_name):
 3         yield line
 4 
 5 def gen_words(line):
 6     for word in (w for w in line.split() if w.strip()):
 7         yield word
 8 
 9 def count_words(file_name):
10     word_map = {}
11     for line in gen_data_from_file(file_name):
12         for word in gen_words(line):
13             if word not in word_map:
14                 word_map[word] = 0
15             word_map[word] += 1
16     return word_map
17 
18 def count_total_chars(file_name):
19     total = 0
20     for line in gen_data_from_file(file_name):
21         total += len(line)
22     return total
23     
24 if __name__ == '__main__':
25     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     print 'do', a
3     CallBackMgr.callback(5, lambda a = a: post_do(a))
4 
5 def post_do(a):
6     print 'post_do', a

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

1 @yield_dec
2 def do(a):
3     print 'do', a
4     yield 5
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())

<span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> tick(self):
    cur </span>=<span style="color: rgba(0, 0, 0, 1)"> time.time()
    </span><span style="color: rgba(0, 0, 255, 1)">for</span> gene, t <span style="color: rgba(0, 0, 255, 1)">in</span><span style="color: rgba(0, 0, 0, 1)"> self.generator_dict.items():
        </span><span style="color: rgba(0, 0, 255, 1)">if</span> cur &gt;=<span style="color: rgba(0, 0, 0, 1)"> t:
            self._do_resume_genetator(gene,cur)

</span><span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> _do_resume_genetator(self,gene, cur ):
    </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)">:
        self.on_generator_excute(gene, cur)
    </span><span style="color: rgba(0, 0, 255, 1)">except</span><span style="color: rgba(0, 0, 0, 1)"> StopIteration,e:
        self.remove_generator(gene)
    </span><span style="color: rgba(0, 0, 255, 1)">except</span><span style="color: rgba(0, 0, 0, 1)"> Exception, e:
        </span><span style="color: rgba(0, 0, 255, 1)">print</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">unexcepet error</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, type(e)
        self.remove_generator(gene)

</span><span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> add_generator(self, gen, deadline):
    self.generator_dict[gen] </span>=<span style="color: rgba(0, 0, 0, 1)"> deadline

</span><span style="color: rgba(0, 0, 255, 1)">def</span><span style="color: rgba(0, 0, 0, 1)"> remove_generator(self, gene):
    </span><span style="color: rgba(0, 0, 255, 1)">del</span><span style="color: rgba(0, 0, 0, 1)"> self.generator_dict[gene]

</span><span style="color: rgba(0, 0, 255, 1)">def</span> on_generator_excute(self, gen, cur_time =<span style="color: rgba(0, 0, 0, 1)"> None):
    t </span>=<span style="color: rgba(0, 0, 0, 1)"> gen.next()
    cur_time </span>= cur_time <span style="color: rgba(0, 0, 255, 1)">or</span><span style="color: rgba(0, 0, 0, 1)"> time.time()
    self.add_generator(gen, t </span>+<span style="color: rgba(0, 0, 0, 1)"> 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)

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> gen
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> _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 是不能嵌套的!

 1 def visit(data):
 2     for elem in data:
 3         if isinstance(elem, tuple) or isinstance(elem, list):
 4             visit(elem) # here value retuened is generator
 5         else:
 6             yield elem
 7             
 8 if __name__ == '__main__':
 9     for e in visit([1, 2, (3, 4), 5]):
10         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加入的

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

 

(2)generator function 中使用 return

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

 1 def gen_with_return(range_num):
 2     if range_num < 0:
 3         return
 4     else:
 5         for i in xrange(range_num):
 6             yield i
 7 
 8 if __name__ == '__main__':
 9     print list(gen_with_return(-1))
10     print list(gen_with_return(1))

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

1 def gen_with_return(range_num):
2     if range_num < 0:
3         return 0
4     else:
5         for i in xrange(range_num):
6             yield i

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

 

References:

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