1. 生成器(generator)
什么是生成器呢,要理解生成器你就必须先理解什么是迭代器,因为生成器也是一种迭代器,是一种更高级、更优雅的迭代器。
关于迭代器,如果你不明白的话,本博文的 前一篇有讲解,这里不再赘述。
首先,明白下面两点:
- 任意生成器都是迭代器 (反之不成立)。
- 任意生成器,都是一个延迟产生值的工厂。
2. yield
Python 有两种不同的方式提供生成器:
现在介绍第一种: 生成器函数
生成器函数 : 包含yield语句的函数。只不过使用 yield
语句来返回结果,而不是return
,yield
语句不像 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. 小结
- 生成器函数语法上与函数类似,差别在于,生成器使用
yield
语句返回一个值。 - 状态挂起,生成器最大的特性在与状态挂起。
- 由于惰性求值的特性,使得内存占用极少。
- 希望大家能掌握生成器,生成器是高手的标配。
参考:
1.知乎: 如何更好的理解Python迭代器和生成器?
2.Python进阶_生成器&生成器表达式
请多多指教 !