Python进阶之生成器

较详细地介绍了生成器的两种生成方式、特性以及方法

Posted by Surflyan on 2017-03-06

1. 生成器(generator)

什么是生成器呢,要理解生成器你就必须先理解什么是迭代器,因为生成器也是一种迭代器,是一种更高级、更优雅的迭代器。
关于迭代器,如果你不明白的话,本博文的 前一篇有讲解,这里不再赘述。
首先,明白下面两点:

  • 任意生成器都是迭代器 (反之不成立)。
  • 任意生成器,都是一个延迟产生值的工厂。

2. yield

Python 有两种不同的方式提供生成器:
现在介绍第一种: 生成器函数
生成器函数 : 包含yield语句的函数。只不过使用 yield 语句来返回结果,而不是returnyield 语句不像 return那样返回值,而是产生多个值,而每次使用yield语句 时产生一个值时,函数就会被 挂起,即函数停留在那点等待被重新唤醒。函数被重新唤醒后就从停止的那点开始执行。这么说你可能还不明白,上例子:

>>> def CheckYield(n):
...     while n > 0:
...         print "before yield"
...         yield n
...         n -= 1
...         print "after yield"
...
>>> ge = CheckYield(2)    #没有执行函数体内语句
>>> ge.next()          #遇到yield,返回值,并暂停
before yield
2
>>> ge.next()        #从上次暂停位置开始继续执行
after yield
before yield
1
>>> yy.next()
after yield            #没有满足条件的值,抛出异常
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> dir(ge)                       #看到__iter__ 和next 了吧,
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'next', 'send', 'throw']

看到了吧,函数会在执行到 yield 语句之后返回一个值,然后下次接着yield 之后的语句运行。

3. 生成器与迭代器的对比

现在我们分别使用迭代器和生成器来写一段打印0-4序列的代码。

3.1 迭代器

class CountIter:
    def __init__(self, n):
        self.n = n
    def __iter__(self):
        self.x = -1
        return self
    def next(self):  # For Python 2.x
        self.x += 1
        if self.x < self.n:
            return self.x
        else:
            raise StopIteration
 for i in CountIter(5):
    print i

CountIter类就是一个迭代器,它的 __iter__()方法返回可迭代对象,next()方法则执行下一次迭代。

3.2 生成器

def count(n):
    x = 0
    while x < n:
        yield x
        x += 1
 for i in count(5):
    print i

什么感觉?最直观的就是生成器写的代码好少。
迭代器每次在执行完 next() 方法并返回之后,该方法的上下文环境就会消失了,所有的next()方法中定义的局变就无法被访问了,在迭代器中,靠self.x+=1来记录next 之后增减。

3.3 惰性求值

对于生成器,每次执行next()方法后,执行到yield关键字处,并将yield后的参数值返回,同时当前生成器函数的上下文会被保留下来。也就是函数内所有变量的状态会被保留,同时函数代码执行到的位置会被保留,像是被挂起一样。这个特点被称为 延迟计算惰性求值(Lazy evaluation),可以有效的节省内存。惰性求值实际上是现实了协同程序 的思想。

协同程序:是一个可以独立运行的函数调用,该调用可以被暂停或者挂起,之后还能够从程序流挂起的地方继续或重新开始。当协同程序被挂起时,Python 就能够从该协同程序中获取一个处于中间状态的属性的返回值(由 yield 返回),当调用 next() 方法使得程序流回到协同程序中时,能够为其传入额外的或者是被改变了的参数,并且从上次挂起的下一条语句继续执行。这是一种类似于进程中断的函数调用方式。这种挂起函数调用并在返回属性中间值后,仍然能够多次继续执行的协同程序被称之为生成器

需要注意的是,生成器也是只能迭代一次 。如上面的count类,试试这样做:

lst=count(5)
print list(lst)
print list(lst)
#结果:
[0, 1, 2, 3, 4]
[]

4.生成器表达式

列表解析使用起来非常方便,可是,它必须一次性生成所有的数据,用来创建列表对象。
因此,生成器根据列表解析,结合自身每次返回一个值,然后挂起的特点,解决这个问题。

列表解析:
[expr for iter_var in iterable if cond_expr]

>>>lst=[x*x for x in range(5)]
>>>lst
[0,1,4,9,16]

生成器表达式:
(expr for iter_var in iterable if cond_expr)

>>>ge=(x*x for x in range(5))
>>>ge
<generator object <genexpr> at 0x03E0C8C8>
>>>ge.next()
0
>>>lsit(ge)
[1,4,9,16]

两者的语法相似,但生成器表达式返回的不是一个列表类型对象,而是一个生成器对象,生成器是一个内存使用友好的结构。

5. 生成器方法

生成器的新特性是在开始运行后为生成器提供值的能力,表现为和生成器和”外部世界“交流的渠道。

5.1 close()方法

close()方法就是关闭生成器。生成器被关闭后,再调用next()方法,会立即抛出StopIteration异常。

>>> ge = (x for x in range(5))
>>> ge.close()
>>> ge.next()
Traceback (most recent call last):
  File "<pyshell#9>", line 1, in <module>
    ge.next()
StopIteration

5.2 send()方法

这或许是生成器最重要的方法,通过 send()想生成器内部传递参数。

def count(n):
    x = 0
    while x < n:
        value = yield x
        if value is not None:
            print 'Hello,%s' %value
        x += 1

运行结果:

>>>ge=count(2)
>>>ge.next()
0
>>>ge.send("Surflyan")
Hello,Surflyan
1
>>>

6. 小结

  1. 生成器函数语法上与函数类似,差别在于,生成器使用yield语句返回一个值。
  2. 状态挂起,生成器最大的特性在与状态挂起。
  3. 由于惰性求值的特性,使得内存占用极少。
  4. 希望大家能掌握生成器,生成器是高手的标配。

参考

1.知乎: 如何更好的理解Python迭代器和生成器?
2.Python进阶_生成器&生成器表达式

请多多指教 !