python进阶之生成器与yield

来源:互联网 发布:刘若鹏 知乎 编辑:程序博客网 时间:2024/05/23 23:34

前言

没有用过的东西,没有深刻理解的东西很难说自己会,而且被别人一问必然破绽百出。虽然之前有接触过python协程的概念,但是只是走马观花,这两天的一次交谈中,别人问到了协程,顿时语塞,死活想不起来曾经看过的东西,之后突然想到了yield,但为时已晚,只能说概念不清,所以本篇先缕缕python的生成器和yield关键字。

什么是生成器

  • 生成器是一个特殊的程序,可以被用作控制循环的迭代行为
  • 生成器类似于返回值为数组的一个函数,这个函数可以接收参数,可以被调用,但是,不同于一般的函数会一次性返回包含了所有数值的数组,生成器一次只产生一个值,这样消耗的内粗数量大大减少,而且允许调用函数可以很快的开始处理前几个返回值。因此,生成器看起来像一个函数但是表现的却像一个迭代器。

python中的生成器

python提供了两种基本的方式。

  • 生成器函数:也是用def来定义,利用关键字yield一次返回一个结果,阻塞,重新开始
  • 生成器表达式:返回一个对象,这个对象只有在需要的时候才产生结果

下面详细讲解。

生成器函数

为什么叫生成器函数?因为他随着时间的推移生成了一个数值队列。一般的函数在执行完毕之后会返回一个值然后退出,但是生成器函数会自动挂起,然后重新拾起继续执行,他会利用yield关键字关起函数,给调用者返回一个值,同时保留了当前的足够多的状态,可以使函数继续执行。生成器和迭代协议是密切相关的,可迭代的对象都有一个__next()__成员方法,这个方法要么返回迭代的下一项,要么引起异常结束迭代。
为了支持迭代协议,拥有yield语句的函数被编译为生成器,这类函数被调用时返回一个生成器对象,返回的对象支持迭代接口,即成员方法__next()__继续从中断处执行执行。
看下面的例子:

# codesdef create_counter(n):    print "create counter"    while True:        yield n        print 'increment n'        n += 1cnt = create_counter(2)print cntprint next(cnt)print next(cnt)# output<generator object create_counter at 0x0000000001D141B0>create counter2increment n3

分析一下这个例子:

  • 在create_counter函数中出现了关键字yield,预示着这个函数每次只产生一个结果值,这个函数返回一个生成器(通过第一行输出可以看出来),用来产生连续的n值
  • 在创造生成器实例的时候,只需要像普通函数一样调用就可以,但是这个调用却不会执行这个函数,这个可以通过输出看出来
  • next()函数将生成器对象作为自己的参数,在第一次调用的时候,他执行了create_counter()函数到yield语句,返回产生的值2
  • 我们重复的调用next()函数,每次他都会从上次被挂起的地方开始执行,直到再次遇到了yield关键字

为了更加深刻的理解,我们再举一个例子。

#codingdef cube(n):    for i in range(n):        yield i ** 3for i in cube(5):    print i#output0182764
所以从理解函数的角度出发我们可以将yield类比为return,但是功能确实完全不同,在for循环中,会自动遵循迭代规则,每次调用next()函数,所以上面的结果不难理解。生成器表达式:生成器表达式来自于迭代和列表解析的组合,关于列表解析的概念和用法可以参见我之前的博客,生成器表达式和列表解析类似,但是他使用尖括号而不是方括号括起来的。如下代码:>>> # 列表解析生成列表>>> [ x ** 3 for x in range(5)][0, 1, 8, 27, 64]>>> >>> # 生成器表达式>>> (x ** 3 for x in range(5))<generator object <genexpr> at 0x000000000315F678>>>> # 两者之间转换>>> list(x ** 3 for x in range(5))[0, 1, 8, 27, 64]就操作而言,生成器表如果使用大量的next()函数会显得十分不方便,for循环会自动出发next函数,所以可以按下面方式使用:>>> for n in (x ** 3 for x in range(5)):    print('%s, %s' % (n, n * n))    0, 01, 18, 6427, 72964, 4096>>> 

两者比较

一个迭代既可以被写成生成器函数,也可以被协程生成器表达式,均支持自动和手动迭代。而且这些生成器只支持一个active迭代,也就是说生成器的迭代器就是生成器本身。

yield总结:

  • 通常的for...in...循环中,in后面是一个数组,这个数组就是一个可迭代对象,类似的还有链表,字符串,文件。它可以是mylist = [1, 2, 3],也可以是mylist = [x*x for x in range(3)]。它的缺陷是所有数据都在内存中,如果有海量数据的话将会非常耗内存。
  • 生成器是可以迭代的,但只可以读取它一次。因为用的时候才生成。比如 mygenerator = (x*x for x in range(3)),注意这里用到了(),它就不是数组,而上面的例子是[]。
  • 我理解的生成器(generator)能够迭代的关键是它有一个next()方法,工作原理就是通过重复调用next()方法,直到捕获一个异常。可以用上面的mygenerator测试。
  • 带有 yield 的函数不再是一个普通函数,而是一个生成器generator,可用于迭代,工作原理同上。
  • yield 是一个类似 return 的关键字,迭代一次遇到yield时就返回yield后面(右边)的值。重点是:下一次迭代时,从上一次迭代遇到的yield后面的代码(下一行)开始执行。
  • 简要理解:yield就是 return 返回一个值,并且记住这个返回的位置,下次迭代就从这个位置后(下一行)开始。
  • 带有yield的函数不仅仅只用于for循环中,而且可用于某个函数的参数,只要这个函数的参数允许迭代参数。比如array.extend函数,它的原型是array.extend(iterable)。
  • send(msg)与next()的区别在于send可以传递参数给yield表达式,这时传递的参数会作为yield表达式的值,而yield的参数是返回给调用者的值。——换句话说,就是send可以强行修改上一个yield表达式值。比如函数中有一个yield赋值,a = yield 5,第一次迭代到这里会返回5,a还没有赋值。第二次迭代时,使用.send(10),那么,就是强行修改yield 5表达式的值为10,本来是5的,那么a=10
  • send(msg)与next()都有返回值,它们的返回值是当前迭代遇到yield时,yield后面表达式的值,其实就是当前迭代中yield后面的参数。
  • 第一次调用时必须先next()或send(None),否则会报错,send后之所以为None是因为这时候没有上一个yield(根据第8条)。可以认为,next()等同于send(None)。
  • 代码示例1:#encoding:UTF-8  def yield_test(n):      for i in range(n):          yield call(i)          print("i=",i)      #做一些其它的事情          print("do something.")          print("end.")    def call(i):      return i*2    #使用for循环  for i in yield_test(5):      print(i,",")  结果是:>>>   0 ,  i= 0  2 ,  i= 1  4 ,  i= 2  6 ,  i= 3  8 ,  i= 4  do something.  end.  >>> 理解的关键在于:下次迭代时,代码从yield的下一跳语句开始执行。代码示例2:def node._get_child_candidates(self, distance, min_dist, max_dist):    if self._leftchild and distance - max_dist < self._median:        yield self._leftchild    if self._rightchild and distance + max_dist >= self._median:        yield self._rightchild与前面不同的是,这个函数中没有for循环,但它依然可以用于迭代。node._get_child_candidates函数中有yield,所以它变成了一个迭代器,可以用于迭代。执行第一次迭代时(其实就是调用next()方法),如果有左节点并且距离满足要求,会执行第一个yield,这时会返回self._leftchild并完成第一个迭代。执行第二次迭代时,从第一个yield后面开始,如果有右节点并且距离满足要求,会执行第二个yield,这时会返回self._rightchild并完成第一个迭代。执行第三次迭代时,第二个yield后再无代码,捕获异常,退出迭代。调用过程:result, candidates = list(), [self]while candidates:    node = candidates.pop()    distance = node._get_dist(obj)    if distance <= max_dist and distance >= min_dist:        result.extend(node._values)    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))return result上面的node._get_child_candidates(self, distance, min_dist, max_dist)是放在extend()函数中作为参数的,为什么可以这么用,就因为extend函数的参数不仅仅支持array,只要它是一个迭代器就可以。它的原型是array.extend(iterable)。

    以上内容转载自:【1】http://www.cnblogs.com/cotyb/p/5260032.html

                                 【2】http://www.jianshu.com/p/d09778f4e055

    如有侵权,实属无意,烦请联系,随机删除。


    原创粉丝点击