Python中yield的作用:迭代生成器

整理自公众号:Devtogether

yield

为了搞清楚 yield 和 return 这两者的区别,我们先来看一个简单的例子:

>>> def self_return(n):
...    print('rocky')
...    while n > 0:
...            print('before return')
...            return n
...            n -= 1
...            print('after return')
...
>>> s = self_return(3)
rocky
before return
>>> s
3

从上面的例子中函数 self_return(n) 被调用的过程中我们可以清晰的看出,s = self_return(3) 函数体内的语句就开始执行了,遇到 return 以后将值返回,并结束在函数体内的执行,所以我们看到的结果是 return 后面的语句根本没有执行,这个是 return 的特点,不知道你还记得么?如果不记得的话可以去翻我前面的文章。

下面我们来将 return 换乘 yield ,再来试试看:

>>> def self_yield(n):
...    print('rocky')
...    while n > 0:
...            print('before yield')
...            yield n
...            n -= 1
...            print('after yield')
...
>>> s = self_yield(3)
>>> s.__next__()
rocky
before yield
3

仔细观察上面的例子你会发现,s = self_yield(n) 并没有去执行函数体内的语句,且 s.next() 的时候遇到 yield 的时候,会返回值,并且暂停。我们接着再继续来试一下:

>>> s.__next__()
after yield
before yield
2
>>> s.__next__()
after yield
before yield
1
>>> s.__next__()
after yield
Traceback (most recent call last):
 File "", line 1, in 
StopIteration

通过上面的继续操作,我们可以看到每次遇到 yield 的时候都会返回值,并且暂停,下次再执行的时候是从上次暂停的位置开始又继续执行的,当没有满足条件的值,就会抛出异常。

结合上面的分析和对用例的执行结果,相信你已经你已经理解了 yield 的特点,也知道它与 return 之间的区别了:一般的函数,都是止于 return;作为生成器的函数,因为有了 yield,则遇到它会挂起。

下面我想再用一个例子来具体的阐述一下。斐波那契数列相信你们已经不陌生了,我在前面的文章中不止一次的提过它,这次我们尝试将 yield 应用到斐波那契数列中:

def fibs(max):
   """
   fibonacci sequence generator
   """
   n, a, b = 0, 0, 1
   while n < max:
       yield b
       a, b = b, a + b
       n += 1

if __name__ == "__main__":
   f = fibs(9)
   for i in f:
       print(i,end = ' ')

上述代码的运行结果如下:

1 1 2 3 5 8 13 21 34 55

你看,用生成器生成的斐波那契数列是不是跟以前的不一样了呢?如果有兴趣的话,你可以将我在前面文章中演示过的斐波那契数列的实现方式和现在的做一下对比,然后仔细观察一下差异之处。

经过这几次的各种演示,其实已经很明确了:在一个函数中如果有了 yield 语句,那么它就是生成器,即也是迭代器。这种方式比前面写迭代器的类要简便的多,但这不是说迭代器不好,无论是使用迭代器还是生成器都要具体问题具体分析。

yield 的作用是在调用的时候返回相应的值,一次返回一个结果,在每个结果中间挂起函数的状态(即暂停执行),下一次执行是从上次暂停的位置开始,继续向下执行。

下面我们来做一道题,要求写出「将一个全是整数的列表进行操作后只保留奇数」。相信大多数人都能很快的写出下面这样的函数:

def get_odd(lst):
   res = []
   for i in lst:
       if i % 2:
           res.append(i)
   return res

def main():
   lst = range(10)
   for i in get_odd(lst):
       print(i)

if __name__ == '__main__':
   main()

上面这个没什么难度,既然我们学了「生成器」,我在前面还这么舔它,是不是我们该用生成器来做一下这道题?看看用生成器来做同样的功能,到底有什么不同:

def get_odd(lst):
   for i in lst:
       if i % 2:
           yield i

def main():
   lst = range(10)
   for i in get_odd(lst):
       print(i)

if __name__ == '__main__':
   main()

对比一下这个功能的两种做法,使用「生成器」以后,代码变的行数更少了(省去了对 res 的操作,不用把结果存在 res 里),代码整体看起来更清晰了(一看就知道干嘛的,不用一上来去想 res 是个什么鬼,append 进去的是个什么玩意儿)。

2.生成器表达式

「生成器表达式」和列表推导式类似。区别在于使用列表推导,一次会产生所有的结果,而用「生成器表达式」则不会这样,它是按需产生。

列表推导式的写法如下:

>>> res = [x for x in range(5)]
>>> res
[0, 1, 2, 3, 4]

生成器表达式就是将上面的 [] 变成 () 即可:

>>> res = (x for x in range(5))
>>> res
 at 0x109d9f570>
>>> next(res)
0
>>> next(res)
1
>>> next(res)
2
>>> next(res)
3

我们也顺便简单的看一下「生成器」的优势在「生成器表达式」中是怎么体现的。如果我们想对一系列整数求和,直接用生成器可以写成下面这样:

>>> sum((x for x in range(5)))
10

当然为了方便起见,也可以省略圆括号,即写成下面这样:

>>> sum(x for x in range(5))
10

但是如果你用常规的写法去写,就会写成下面这样:

>>> sum([x for x in range(5)])
10

上面的代码先构造了一个列表,然后再用 sum 函数求和,多了一步,天差地别,光在时间效率上,就已经输掉了裤子。

所以综合上面文章所讲,「生成器」光在明面上的优点就有好几个:代码行数更少;代码更易读;时效更高...

你可能感兴趣的:(Python中yield的作用:迭代生成器)