python yield 进阶(一)

时间:2022-10-12 12:59:56


PS:硬说原创 我只能说自己太不要脸了 就当是个搬运工吧 希望对您有帮助

先来看看基础的---重头戏在后面:


yield的英文单词意思是生产,刚接触Python的时候感到非常困惑,一直没弄明白yield的用法。






只是粗略的知道yield可以用来为一个函数返回值塞数据,比如下面的例子:

def  addlist(alist):
     for  i  in  alist:
         yield  i  +   1

取出alist的每一项,然后把i + 1塞进去。然后通过调用取出每一项:


alist  =  [ 1 ,  2 ,  3 ,  4 ]
for  x  in  addlist(alist):
     print  x,

这的确是yield应用的一个例子,但是,看过limodou的文章《

​2.5版yield之学习心得​​》,并自己反复体验后,对yield有了一个全新的理解。


1. 包含yield的函数

假如你看到某个函数包含了yield,这意味着这个函数已经是一个Generator,它的执行会和其他普通的函数有很多不同。比如下面的简单的函数:


def  h():
     print   ' To be brave '
     yield   5

h()

可以看到,调用h()之后,print 语句并没有执行!这就是yield,那么,如何让print 语句执行呢?这就是后面要讨论的问题,通过后面的讨论和学习,就会明白yield的工作原理了。


2. yield是一个表达式

Python2.5以前,yield是一个语句,但现在2.5中,yield是一个表达式(Expression),比如:


m  =   yield   5

表达式(yield 5)的返回值将赋值给m,所以,认为 m = 5 是错误的。那么如何获取(yield 5)的返回值呢?需要用到后面要介绍的send(msg)方法。


3. 透过next()语句看原理

现在,我们来揭晓yield的工作原理。我们知道,我们上面的h()被调用后并没有执行,因为它有yield表达式,因此,我们通过next()语句让它执行。next()语句将恢复Generator执行,并直到下一个yield表达式处。比如:


def  h():
     print   ' Wen Chuan '
     yield   5
     print   ' Fighting! '

c  =  h()
c.next()

c.next()调用后,h()开始执行,直到遇到yield 5,因此输出结果:


Wen Chuan


当我们再次调用c.next()时,会继续执行,直到找到下一个yield表达式。由于后面没有yield了,因此会拋出异常:


Wen Chuan
Fighting!
Traceback (most recent call last):
  File  " /home/evergreen/Codes/yidld.py " , line  11 ,  in   < module >
    c.next()
StopIteration



4. send(msg) 与 next()

了解了next()如何让包含yield的函数执行后,我们再来看另外一个非常重要的函数send(msg)。其实next()和send()在一定意义上作用是相似的,区别是send()可以传递yield表达式的值进去,而next()不能传递特定的值,只能传递None进去。因此,我们可以看做


c.next() 和 c.send(None) 作用是一样的。


来看这个例子:


def  h():
     print   ' Wen Chuan ' ,
    m  =   yield   5    #  Fighting!
     print  m
    d  =   yield   12
     print   ' We are together! '

c  =  h()
c.next()   # 相当于c.send(None)
c.send( ' Fighting! ' )   # (yield 5)表达式被赋予了'Fighting!'

输出的结果为:


Wen Chuan Fighting!


需要提醒的是,第一次调用时,请使用next()语句或是send(None),不能使用send发送一个非None的值,否则会出错的,因为没有yield语句来接收这个值。


5. send(msg) 与 next()的返回值

send(msg) 和 next()是有返回值的,它们的返回值很特殊,返回的是下一个yield表达式的参数。比如yield 5,则返回 5 。到这里,是不是明白了一些什么东西?本文第一个例子中,通过for i in alist 遍历 Generator,其实是每次都调用了alist.Next(),而每次alist.Next()的返回值正是yield的参数,即我们开始认为被压进去的东东。我们再延续上面的例子:


def  h():
     print   ' Wen Chuan ' ,
    m  =   yield   5    #  Fighting!
     print  m
    d  =   yield   12
     print   ' We are together! '

c  =  h()
m  =  c.next()   # m 获取了yield 5 的参数值 5
d  =  c.send( ' Fighting! ' )   # d 获取了yield 12 的参数值12
print   ' We will never forget the date ' , m,  ' . ' , d

输出结果:


Wen Chuan Fighting!


We will never forget the date 5 . 12


6. throw() 与 close()中断 Generator

中断Generator是一个非常灵活的技巧,可以通过throw抛出一个GeneratorExit异常来终止Generator。Close()方法作用是一样的,其实内部它是调用了throw(GeneratorExit)的。我们看:


def  close(self):
     try :
        self.throw(GeneratorExit)
     except  (GeneratorExit, StopIteration):
         pass
     else :
         raise  RuntimeError( " generator ignored GeneratorExit " )
#  Other exceptions are not caught

因此,当我们调用了close()方法后,再调用next()或是send(msg)的话会抛出一个异常:


Traceback (most recent call last):
  File  " /home/evergreen/Codes/yidld.py " , line  14 ,  in   < module >
    d  =  c.send( ' Fighting! ' )   # d 获取了yield 12 的参数值12
StopIteration

第二部分是干货:



协程与子例程



我们调用一个普通的Python函数时,一般是从函数的第一行代码开始执行,结束于return语句、异常或者函数结束(可以看作隐式的返回None)。一旦函数将控制权交还给调用者,就意味着全部结束。函数中做的所有工作以及保存在局部变量中的数据都将丢失。再次调用这个函数时,一切都将从头创建。 



对于在计算机编程中所讨论的函数,这是很标准的流程。这样的函数只能返回一个值,不过,有时可以创建能产生一个序列的函数还是有帮助的。要做到这一点,这种函数需要能够“保存自己的工作”。 



我说过,能够“产生一个序列”是因为我们的函数并没有像通常意义那样返回。return隐含的意思是函数正将执行代码的控制权返回给函数被调用的地方。而"yield"的隐含意思是控制权的转移是临时和自愿的,我们的函数将来还会收回控制权。



 



在Python中,拥有这种能力的“函数”被称为生成器,它非常的有用。生成器(以及yield语句)最初的引入是为了让程序员可以更简单的编写用来产生值的序列的代码。 以前,要实现类似随机数生成器的东西,需要实现一个类或者一个模块,在生成数据的同时保持对每次调用之间状态的跟踪。引入生成器之后,这变得非常简单。

生成值的序列。

注意:在Python之外,最简单的生成器应该是被称为协程(​​coroutines​​)的东西。在本文中,我将使用这个术语。请记住,在Python的概念中,这里提到的协程就是生成器。Python正式的术语是生成器;协程只是便于讨论,在语言层面并没有正式定义。


 


处理无限序列

噢,真是如此吗?过了几天,老板过来告诉我们她遇到了一些小问题:她打算把我们的get_primes函数用于一个很大的包含数字的list。实际上,这个list非常大,仅仅是创建这个list就会用完系统的所有内存。为此,她希望能够在调用get_primes函数时带上一个start参数,返回所有大于这个参数的素数(也许她要解决 ​​Project Euler problem 10​​)。

(虽然有很多有用的应用程序可以用来操作无限序列)。看上去用普通函数处理这个问题的可能性比较渺茫

函数只有一次返回结果的机会,因而必须一次返回所有的结果。得出这样的结论似乎毫无意义;“函数不就是这样工作的么”,通常我们都这么认为的。可是,不学不成,不问不知,“如果它们并非如此呢?”

想象一下,如果get_primes可以只是简单返回下一个值,而不是一次返回全部的值,我们能做什么?我们就不再需要创建列表。没有列表,就没有内存的问题。由于老板告诉我们的是,她只需要遍历结果,她不会知道我们实现上的区别。

函数只有一次返回结果的机会,因而必须一次返回所有的结果。得出这样的结论似乎毫无意义;“函数不就是这样工作的么”,通常我们都这么认为的。可是,不学不成,不问不知,“如果它们并非如此呢?”

想象一下,如果get_primes可以只是简单返回下一个值,而不是一次返回全部的值,我们能做什么?我们就不再需要创建列表。没有列表,就没有内存的问题。由于老板告诉我们的是,她只需要遍历结果,她不会知道我们实现上的区别。