Python学习(二)—— Generator

Python中有个generator的概念,之前走马观花的看API的时候觉得就是一个可自定义的不依赖集合数据的迭代器。直到碰到 yield 的时候,也就是自己开始定义generator function的时候,才开始重视所谓python generator。

1. Genertor中三个概念的区分

generator/generator function

准确来说,就是一个函数定义——函数体中使用了yield表达式(yield expression)的函数定义。yield表达式是generator函数定义的充要条件。
所以,完整地来讲,generator 应该叫做_generator function_。而且official document中也指出,在有的上下文中有时generator,指的其实是**generator iterator,所以建议表述的时候尽量使用完整描述——generator function**。

另外,generator的目的是为了在for-loop中能依次返回一系列值,或者在每次调用next()方法时能返回当前迭代值。因此,如下面例子仅仅是在function定义中随便使用一下yield表达式,是达不到此目的的,需要一组调用yield语句的代码逻辑。
Example:

>>> def hello_generator():
...     yield 'hello generator'
...
>>> hello_generator
<function hello_generator at 0x10d50e488>
generator iterator

当generator function被调用时,即会创建一个generator object并返回,这个generator object就叫做**generator iterator**。
它的部分特性可以理解为迭代器,同时isinstance Iterator和Iteratable,主要负责记忆内部执行状态,调用它的 __next__() 或者 send() 方法,可以让其开始执行generator function函数体,直到遇到第一个yiled表达式为止。这时执行yield表达式,并且返回给next或者send方法的调用者。
之后再次调用next或者send方法时,则会从generator function函数体中上一次yield表达式暂停的位置开始执行,直到遇到下一个yield表达式。
Example:

>>> def hello_generator(value=1):
...     while(value):
...         yield value
...         value-=1
...
>>> hello_generator
<function hello_generator at 0x10d50e598>
>>> a = hello_generator(3)
>>> a
<generator object hello_generator at 0x10d506a40>
>>> next(a)
3
>>> next(a)
2
>>> next(a)
1
>>> next(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

注意上文事例,当方法体执行中已经没有下一个yield,即没有下一个iterm时,generator iterator则会抛出__StopIteration__ exception。

generator expression

是一种简易式生成generator iterator的方式,不需要编写generator function,因此也用不到yield表达式。语法书写方式与列表生成式(list comprehensions)相似,即使将中括号改为小括号即可。

>>> a = [x*x for x in range(5)]
>>> a
[0, 1, 4, 9, 16]
>>> b = (x*x for x in range(5))
>>> b
<generator object <genexpr> at 0x10d506a40>

2. yield expression

使用在generator function函数体中,用于返回每一次迭代的结果。可以理解为__return__,但是一个_magic return_。

通常我们调用一个函数,函数将有且只有一个返回值,并且当return语句返回的时候,整个函数内部的执行状态和内部函数都将释放。当再次调用这个函数时,一切将从头创建,也可以认为这个函数被_refresh_。
Example: (以常用的斐波那契数列为例)

>>> def fib(count):
...     index, a, b, list = 0, 0, 1, []
...     while index < count:
...         list.append(a)
...         a, b = b, a + b
...         index += 1
...     return list
>>> if fib(15)[14] > 200:
...     array = fib(50)
...
>>> array
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976, 7778742049]
>>> array[49]
7778742049

上面这个例子可以看到是用return实现的,在这段逻辑中,为了查看第15位值是否大于200,大于的话则获取第50位数值。

  • 首先,在函数内部实现时需要一个可变长的list容器(且根据参数count的不同,这个List的数据量可能非常的大)。这其实就是generator想解决的一个问题:当想生成一系列数据时,如果一次性的算出所有数据塞入一个list中保存(如count=1M),这会受限于内存大小,并且需要使用的也许仅是前面几个数据,那就后面的一批数据的计算量就白白浪费了。

  • 其次,15位之前的数据其实一共运算了两次,这是因为调用了两次fib()函数,而且不得不调用两次。
    当然这里并不是想从performance的角度来讲问题。从语法实现来看,函数一旦return即结束当前正在执行函数的调用,将控制权和return的值返还给函数被调用的地方。一切有关这个函数曾经运算过的状态都将被遗忘。_yield_语句,则不会让generator遗忘状态。

如果是用 yield 和generator来实现呢?
Examp:

>>> def fib(count):
...     index, a, b = 0, 0, 1
...     while index < count:
...         print("Calculate the number position: %d……" % index)
...         yield a
...         a, b = b, a + b
...         index += 1
>>> generator = fib(50)
>>> outer_index = 0
>>> for item in generator:
...     print("outer function control")
...     outer_index +=1
...     if outer_index == 15:
...             if item <= 200:
...                     break
... else:
...     print(item)
...
outer function control
Calculate the number position: 0……
outer function control
Calculate the number position: 1……
outer function control
Calculate the number position: 2……
……
outer function control
Calculate the number position: 48……
outer function control
Calculate the number position: 49……
7778742049

用for-loop去遍历iterator对象的时候,其实每一次迭代就是去调用了一次next()函数,所以这里可以清晰的看到,如果第15位值不满足大于200的要求时,整个迭代即停止了。而每一次下一位数值的计算,是由当次迭代的next()函数出发的。

没有利用list空间存储来进行计算,也没有浪费不必要的计算次数,yield 和genertor解决了之前 return 面临的问题。

3. generator object的_send(value)_方法

也许上一个yield的事例,有人来说,以代码自释意来说,仍要假设它需要计算到第50位,自释效果并不怎么好。有没有一种方式让某次迭代中,外部依然可以传参的?
这时候,就看generator object的_send(value)_方法和yield statement的结合使用了。

Example:(修改上面的generator function和调用方式)

>>> def fib(count):
...     index, a, b = 0, 0, 1
...     while index < count:
...         print("Calculate the number position: %d……" % index)
...         #注意,添加了input这段代码
...         input = yield a
...         if input:
...             count = input
...
...         a, b = b, a + b
...         index += 1
...
>>> outer_index, input, generator, item  = 0, None, fib(15), None
>>> try:
...     while True:
...         print("outer function control")
...         outer_index +=1
...         if outer_index == 15:
...             input = 50
...         item = generator.send(input)
... except StopIteration:
...     print(item)
...
outer function control
Calculate the number position: 0……
outer function control
Calculate the number position: 1……
……
outer function control
Calculate the number position: 48……
outer function control
Calculate the number position: 49……
7778742049

在generator function需要注意的就是input = yield a, 这里的input值,其实是下一次迭代启动时,send(value)调用时传进来的input值。这条语句可以分两段理解 yield a 本次迭代结束,input = value下次迭代开始。
当然,这里使用send方式调用,则不能再使用for-loop自动迭代,需要每次手动调用send方式出发next迭代。
必须非常注意的是,第一次调用generator.send(value)时,value必须为None,原因是第一次迭代是从generator function函数体的第一条语句开始执行,并没有yield表达式来接收input值。

官方也提供了一个很好的事例代码来理解generator iterator的send(value)方法,以及其他方法。官网事例

>>> def echo(value=None):
... print("Execution starts when 'next()' is called for the first time.")
... try:
... while True:
... try:
... value = (yield value)
... except Exception as e:
... value = e
... finally:
... print("Don't forget to clean up when 'close()' is called.")
... >>> generator = echo(1)
>>> print(next(generator))
Execution starts when 'next()' is called for the first time.
1
>>> print(next(generator))
None
>>> print(generator.send(2))
2
>>> generator.throw(TypeError, "spam")
TypeError('spam',)
>>> generator.close()
Don't forget to clean up when 'close()' is called.

附:

yield表达式还有一种yield from的方式、async generator这里暂时没有提,后面有时间再继续写。

本文中代码事例的场景主要是为了展现generator的特性和实现过程,并不代表真实生产场景。所以,在这里看起来好像generator的代码行数比第一种function return的行数要多而复杂。

转载于:https://my.oschina.net/elleneye/blog/823841

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值