• 一场由yield引发的连串拷问


    最近在学习Python中生成器时,遇到了一个yield关键词,廖雪峰老师的官网中也没有详细的解释,经过一番查阅和研究,终于对它有了一些认识并做了总结(如有不对之处,还请大神指正)。

    首先先简单了解下生成器generator,它是为了弥补类似list生成序列时造成的内存空间浪费,例如下面代码中L会将所有值运算出来,全部放到内存中,可想而知,要是有百万千万级的数据,该占用多大内存。而使用生成器的形式,只要将[]改为(),这样只有需要用到的时候,才会去计算下一个值。

    >>> L = [x * x for x in range(10)]
    >>> L
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    >>> g = (x * x for x in range(10))
    >>> g
    <generator object <genexpr> at 0x0062B568>

    如何取出g中的值,只要调用next()就可以了

    >>> next(g)
    0
    >>> next(g)
    1

    接下来进入正题,我们通过一个实战来讲下yield关键字,让你完全攻克它

     1 def foo(num):
     2     print("foo starting")
     3     while num < 5:
     4         res = yield num
     5         num = num + 1
     6         print('res ::',res)
     7 
     8 print("Starting First")
     9 g = foo(0)
    10 
    11 print("Starting for")
    12 for n in g:
    13     print('n ::',n)

    首先先看下程序执行顺序,按照一般函数理解,3个starting的顺序应该是 Starting First --> foo starting --> Starting for,但执行结果如下

     1 Starting First
     2 Starting for
     3 foo starting
     4 n :: 0
     5 res :: None
     6 n :: 1
     7 res :: None
     8 n :: 2
     9 res :: None
    10 n :: 3
    11 res :: None
    12 n :: 4
    13 res :: None

    那就奇怪了,调用foo()函数动作在for循环之前啊,那么在这里要明确一下,函数中含有yield关键字,那么就不能把它当做一个普通函数了,而是一个生成器,且不会立即执行,只有调用next()时才会执行。

    继续往下看,打印出“Starting for” 之后就进入了foo函数中执行了,也就是进入g生成器中了,可是上面讲过,只有调用next()才会到生成器中取值啊,为什么 for n in g 也会进去呢?

    那就要研究下for n in g这段语句了,其实Python的for循环本质上就是通过不断调用next()函数实现的,也就是说如下两种写法是完全等价的。

     1 listx = [1,2,3,4,5]
     2 
     3 # 循环方法一
     4 for n in listx:
     5     print(n)
     6 
     7 # 等价方法二
     8 glistx = iter(listx)   # 转换为Iterator对象
     9 
    10 while True:
    11     try:
    12         # 获得下一个值:
    13         x = next(glistx)
    14         print(x)
    15     except StopIteration:
    16         # 遇到StopIteration就退出循环
    17         break

    这样就显而易见了,其实for循环中也是调了next()函数,到生成器中去取值的。

    继续往下走,终于到了梦寐以求的yield关键字,怎么理解呢?首先可以先按照return来理解,所以当num=0的时候,走到yield num就直接return了,因此打印出 n :: 0

    继续执行for循环,继续调用next()函数,那么这里就要了解一个知识点,生成器中的next()是接着上次return(yield)的地方继续执行,而不是从头执行,这就是yield与return的区别。此时,由于上一步yield已经返回了0到for循环了,那么res就没有变量来赋值了,也就是None,因此下一步打印的就是res :: None,此时num=num+1变为了1。在while True中循环,又遇到了yield num,那么又返回给了for函数一个1,因此 n接收到了1,打印出n :: 1,后面的以此类推,不再赘述。

    根据上面步骤解读,应该已经明白了yield,next()的含义和用法了吧,那我们再乘胜追击,在for循环中再加个send()函数,这又是什么含义呢?

     1 def foo(num):
     2     print("foo starting")
     3     while num < 5:
     4         res = yield num
     5         num = num + 1
     6         print('res ::',res)
     7 
     8 print("Starting First")
     9 g = foo(0)
    10 
    11 print("Starting for")
    12 for n in g:
    13     g.send(n + 100)
    14     print('n ::',n)

    其实可以这样理解,yield num返回之后,程序下一步是赋值给res,只不过yield返回后变为空了,所以res被赋值None,而send就是解决赋值的问题。send()含义就是继续执行yield之后的赋值操作,也就是重新赋值给res,那么再打印res的话就不是None了,上述执行结果如下:

     1 Starting First
     2 Starting for
     3 foo starting
     4 res :: 100
     5 n :: 0
     6 res :: None
     7 res :: 102
     8 n :: 2
     9 res :: None
    10 res :: 104
    11 Traceback (most recent call last):
    12   File "d:/VSCode/Python/genaratex.py", line 13, in <module>
    13     g.send(n + 100)
    14 StopIteration

    按照如上思路,先赋值res=100,再打印res,再打印n。下个循环后res还是应该先赋值101啊,为什么结果是res :: None 和 res :: 102 呢,这里又有一个知识点,send函数中不仅仅是赋值,并且自带next()函数调用,所以在send内部就执行了一次next(),导致num再次加1,再次执行了yield num,只是for函数里面没有接收变量罢了,因此就会出现打印出res :: None和 res :: 102的输出了。

    如此,代码最后的报错也应该知道为什么了吧,即当n获取生成器中已经是最后一个值的时候,send中再次next(),当然找不到值了,触发了StopIteration异常。

    总结:

    1. 生成器generator可以用()来定义,也可以使用iter()来转换,带yield的函数默认都是generator
    2. next()取值时,除首次外都是从上次yield的地方开始执行的
    3. Python中for循环的本质也是调用next()来逐个取值
    4. send函数是先在yield处进行赋值,再进行next()操作,如果没有进行yield,直接调用send会报错找不到需要赋值的对象。
  • 相关阅读:
    spark
    mongdb
    redis
    mysql
    kylin
    kafka
    hadoop+hive+hbase+kylin
    git
    elasticsearch
    clickhouse
  • 原文地址:https://www.cnblogs.com/potato-find/p/13171465.html
Copyright © 2020-2023  润新知