原文章地址: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表达式的值返回。
网友评论