原文章地址:http://python.jobbole.com/87805/
学习这篇文章之前需要了解——迭代的概念
对于迭代这个词,百度百科是这么翻译的——重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次迭代,每一次迭代得到的结果会作为下一次迭代初始值
学习这篇文章之前需要了解——python assert断言语句,点我跳转
让我们先看一张图,这张图解释了他们之间的关系
容器 (container)
容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个地迭代获取,可以用in, not in关键字判断元素是否包含在容器中。通常这类数据结构把所有的元素存储在内存中(也有一些特例,并不是所有的元素都放在内存,比如迭代器和生成器对象)在Python中,常见的容器对象有:
- list, deque, ….
- set, frozensets, ….
- dict, defaultdict, OrderedDict, Counter, ….
- tuple, namedtuple, …
- str
容器比较容易理解,因为你就可以把它看作是一个盒子、一栋房子、一个柜子,里面可以塞任何东西。从技术角度来说,当它可以用来询问某个元素是否包含在其中时,那么这个对象就可以认为是一个容器,比如 list,set,tuples都是容器对象:
询问某元素是否在dict中用dict的中key:
询问某substring是否在string中:
尽管绝大多数容器都提供了某种方式来获取其中的每一个元素,但这并不是容器本身提供的能力,而是可迭代对象赋予了容器这种能力,当然并不是所有的容器都是可迭代的,比如:Bloom filter,虽然Bloom filter可以用来检测某个元素是否包含在容器中,但是并不能从容器中获取其中的每一个值,因为Bloom filter压根就没把元素存储在容器中,而是通过一个散列函数映射成一个值保存在数组中。
可迭代对象(iterable)
刚才说过,很多容器都是可迭代对象,此外还有更多的对象同样也是可迭代对象,比如处于打开状态的files,sockets等等。但凡是可以返回一个迭代器的对象都可称之为可迭代对象(可迭代对象可以通过iter()方法返回一个迭代器(iterator))。也可以简单的理解为可以直接作用于for循环的对象统称为可迭代对象(Iterable)。听起来可能有点困惑,没关系,先看一个例子:
x = [1,2,3]
y = iter(x)
z = iter(x)
next(y)
Out[23]: 1
next(y)
Out[24]: 2
type(x)
Out[25]: list
type(y)
Out[26]: listiterator
这里x是一个可迭代对象,可迭代对象和容器一样是一种通俗的叫法,并不是指某种具体的数据类型,list是可迭代对象,dict是可迭代对象,set也是可迭代对象。y和z是两个独立的迭代器,迭代器内部持有一个状态,该状态用于记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。迭代器有一种具体的迭代器类型,比如list_iterator,set_iterator。可迭代对象实现了iter方法,该方法返回一个迭代器对象。
当运行代码:
x = [1,2,3]
for i in x:
...
实际执行情况是:
迭代器(iterator)
那么什么迭代器呢?它是一个带状态的对象,他能在你调用next()方法的时候返回容器中的下一个值,任何实现了iter和next()(python2中实现next())方法的对象都是迭代器,iter返回迭代器自身,next返回容器中的下一个值,如果容器中没有更多元素了,则抛出StopIteration异常。
所以,迭代器就是实现了工厂模式的对象,它在你每次你询问要下一个值的时候给你返回。有很多关于迭代器的例子,比如itertools函数返回的都是迭代器对象。
我们自定义一个迭代器
# python2.7
class test:
def __init__(self):
self.x = 0
def __iter__(self):
return self
def next(self):
value = self.x + 1
self.x = value
if self.x > 2:
raise StopIteration
return value
t = test()
a = iter(t)
next(a)
Out[47]: 1
next(a)
Out[67]: 2
next(a)
Traceback (most recent call last):
File "<ipython-input-82-3f6e2eea332d>", line 1, in <module>
next(a)
StopIteration
test既是一个可迭代对象(因为它实现了iter方法),又是一个迭代器(因为实现了next方法)。实例变量x维护迭代器内部的状态。每次调用next()方法的时候做两件事:
- 为下一次调用next()方法修改状态
- 为当前这次调用生成返回结果
迭代器就像一个懒加载的工厂,等到有人需要的时候才给它生成值返回,没调用的时候就处于休眠状态等待下一次调用。
生成器(generator)
我们先来看一段官方对生成器的说明:
Python’s generators provide a convenient way to implement the iterator protocol.
意思大概是,python的生成器是一种以更优雅的方式去实现的迭代器,可见生成器是迭代器的一种。
生成器算得上是Python语言中最吸引人的特性之一,生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅。它不需要再像上面的类一样写iter()和next()方法了,只需要一个yiled关键字。 生成器一定是迭代器(反之不成立),因此任何生成器也是以一种懒加载的模式生成值。用生成器实现上面迭代器的例子是:
def fib():
x = 0
while x<2 :
x += 1
yield x
运行结果:
a = fib()
next(a)
Out[100]: 1
next(a)
Out[101]: 2
next(a)
Traceback (most recent call last):
File "<ipython-input-102-3f6e2eea332d>", line 1, in <module>
next(a)
StopIteration
type(a)
Out[103]: generator
fib就是一个普通的python函数,它特殊的地方在于函数体中没有return关键字,函数的返回值是一个生成器对象。当执行f=fib()返回的是一个生成器对象,此时函数体中的代码并不会执行,只有显示或隐示地调用next的时候才会真正执行里面的代码。
yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。
生成器在Python中是一个非常强大的编程结构,可以用更少地中间变量写流式代码,此外,相比其它容器对象它更能节省内存和CPU,当然它可以用更少的代码来实现相似的功能。
生成器表达式(generator expression)
生成器表达式是列表推倒式的生成器版本,看起来像列表推导式,但是它返回的是一个生成器对象而不是列表对象。
a = (x for x in range(10))
type(a)
Out[106]: generator
next(a)
Out[108]: 0
列表推导式长什么样(将小括号换成中括号就是一个列表推导式了):
b = [x for x in range(10)]
b
Out[110]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
深入学习yield(原文章地址)
def g():
print("1 is")
yield 1
print("2 is")
yield 2
print("3 is")
yield 3
>>>z = g()
>>>next(z)
1 is
1
>>>next(z)
2 is
2
>>>next(z)
3 is
3
>>>next(z)
Traceback (most recent call last):
File "<ipython-input-166-7b32f85a2b4e>", line 1, in <module>
next(z)
StopIteration
第一次调用next()方法时,函数似乎执行到yield 1,就暂停了。然后再次调用next()时,函数从yield 1之后开始执行的,并再次暂停。第三次调用next(),从第二次暂停的地方开始执行。第四次,抛出StopIteration异常。
事实上,generator确实在遇到yield之后暂停了,确切点说,是先返回了yield表达式的值,再暂停的。当再次调用next()时,从先前暂停的地方开始执行,直到遇到下一个yield。这与上文介绍的对iterator调用next()方法,执行原理一般无二。
有些教程里说generator保存的是算法,而我觉得用中断服务子程序来描述generator或许能更好理解,这样你就能将yield理解成一个中断服务子程序的断点,没错,是中断服务子程序的断点。我们每次对一个generator对象调用next()时,函数内部代码执行到”断点”yield,然后返回这一部分的结果,并保存上下文环境,”中断”返回。
我们再来看另一段代码。
def gen():
while True:
s = yield
print(s)
>>> g = gen()
>>> g.send('hello') #这里很重要,向一个刚开始的生成器直接send一个值,会报错,所以我们得先调用next方法
#让生成器向后移动一个位置后再send值
Traceback (most recent call last):
File "<ipython-input-169-9d017dfb1443>", line 1, in <module>
g.send('hello')
TypeError: can't send non-None value to a just-started generator
# 下面才是send函数正确的使用方法
>>> next(g)
>>> g.send('hello')
hello
我也是看到这个形式的generator,懵了,才想要深入学习generator与yield的。结合以上的知识,我再告诉你,generator其实有第2种调用方法(恢复执行),即通过send(value)方法将value作为yield表达式的当前值,你可以用该值再对其他变量进行赋值,这一段代码就很好理解了。当我们调用send(value)方法时,generator正由于yield的缘故被暂停了。此时,send(value)方法传入的值作为yield表达式的值,函数中又将该值赋给了变量s,然后print函数打印s,循环再遇到yield,暂停返回。
调用send(value)时要注意,要确保,generator是在yield处被暂停了,如此才能向yield表达式传值,否则将会报错(如上所示),可通过next()方法或send(None)使generator执行到yield。
再来看一段yield更复杂的用法,或许能加深你对generator的next()与send(value)的理解。
>>> def echo(value=None):
... while 1:
... value = (yield value)
... print("The value is", value)
... if value:
... value += 1
...
>>> g = echo(1)
>>> next(g)
1
>>> g.send(2)
The value is 2
3
>>> g.send(5)
The value is 5
6
>>> next(g)
The value is None
上述代码既有yield value的形式,又有value = yield形式,看起来有点复杂。但以yield分离代码进行解读,就不太难了。第一次调用next()方法,执行到yield value表达式,保存上下文环境暂停返回1。第二次调用send(value)方法,从value = yield开始,打印,再次遇到yield value暂停返回。后续的调用send(value)或next()都不外如是。
但是,这里就引出了另一个问题,yield作为一个暂停恢复的点,代码从yield处恢复,又在下一个yield处暂停。可见,在一次next()(非首次)或send(value)调用过程中,实际上存在2个yield,一个作为恢复点的yield与一个作为暂停点的yield。因此,也就有2个yield表达式。send(value)方法是将值传给恢复点yield;调用next()表达式的值时,其恢复点yield的值总是为None,而将暂停点的yield表达式的值返回。为方便记忆,你可以将此处的恢复点记作当前的(current),而将暂停点记作下一次的(next),这样就与next()方法匹配起来啦。
小结
- 可迭代对象(Iterable)是实现了iter()方法的对象,通过调用iter()方法可以获得一个迭代器(Iterator)。
- 迭代器(Iterator)是实现了iter()和next()的对象。
- for … in …的迭代,实际是将可迭代对象转换成迭代器,再重复调用next()方法实现的。
- 生成器(generator)是一个特殊的迭代器,它的实现更简单优雅
- ieyield是生成器实现next()方法的关键。它作为生成器执行的暂停恢复点,可以对yield表达式进行赋值,也可以将yield表达式的值返回。