迭代器对象 VS 可迭代对象 VS 生成器
为何在 p y t h o n python python中可以很方便的使用 f o r for for循环来遍历列表、字符串甚至是文件?它的背后实现离不开迭代器和可迭代对象。
迭代器协议
首先来谈谈 p y py py中的迭代器协议:
- 实现内置函数 i t e r iter iter,它返回一个迭代器对象。
- 通过调用迭代器对象的 n e x t next next函数,取得每次迭代的内容。
- 多次调用 n e x t next next函数后,抛出 S t o p I t e r a t i o n StopIteration StopIteration异常(迭代终止)。
可以猜测 p y t h o n python python中 f o r for for循环的语义如下:
lst = [1, 2, 3]
for v in lst:
print v
it = iter(lst)
try:
while True:
v = it.next()
print v
except StopIteration:
pass
从上面的描述中,我们也能看出一些端倪:迭代器对象必须实现
i
t
e
r
iter
iter和
n
e
x
t
next
next函数。那么只实现了
i
t
e
r
iter
iter函数的算什么呢?其实他们就是可迭代对象。可迭代对象也是可以直接用
f
o
r
for
for循环遍历的,因为他们的
i
t
e
r
iter
iter函数会返回一个迭代器对象,只要迭代器对象实现了
n
e
x
t
next
next方法就可以了。也就是说迭代器对象是可迭代对象的子集。
下面通过一段代码来加深对他们的认识:
注:本文中的代码如非特殊标注,运行环境均为 p y t h o n 2.7 python\ 2.7 python 2.7。
from collections import Iterator, Iterable
lst = [1, 2, 3]
list_it = iter(lst)
print isinstance(lst, Iterator)
print isinstance(lst, Iterable)
print isinstance(list_it, Iterator)
print isinstance(list_it, Iterable)
res1 = []
for i in xrange(2):
for j in lst:
res1.append(j)
res2 = []
for i in xrange(2):
for j in list_it:
res2.append(j)
print res1
print res2
可以看出来
p
y
t
h
o
n
python
python内置的列表是一个可迭代对象,但不是迭代器。通过调用其
i
t
e
r
iter
iter函数可以返回一个迭代器对象,而且每次循环都会耗尽一个迭代器对象,因此在大部分情况下,我们不能重复迭代同一个迭代器对象。
下面继续看一段代码:
lst = [1, 2, 3]
list_it = iter(lst)
print next(list_it) # lst[0]
lst[1] = 10
print next(list_it) # lst[1]
lst.pop()
print next(list_it) # the end of list
据此可以猜测出列表的
i
t
e
r
iter
iter函数的实现:无非就是创建了一个新的对象,让其存有自身的引用以及当前的索引位置,然后在
n
e
x
t
next
next函数中递增索引,当达到列表末尾时抛出异常。因为整个过程中没有创建新的列表对象——仅创建了一个迭代器对象,因此空间占用非常少。不过要注意,当原数组发生变化时,迭代器的内容也会发生变换。
接下来,让我们实现自己的迭代器:
# coding=utf-8
from collections import Iterable, Iterator
class TestA(object):
def __init__(self, dis=1, n=3): # 一个等差数列
self.value = 0
self.dis = dis
self.num = n
def __iter__(self):
return self
def next(self):
if self.num == 0:
raise StopIteration
self.num -= 1
tmp = self.value
self.value += self.dis
return tmp
class TestB(object):
def __init__(self, dis=1, n=3):
self.dis = dis
self.num = n
def __iter__(self): # 一般不推荐该做法
return TestA(self.dis, self.num)
a = TestA()
print isinstance(a, Iterable)
print isinstance(a, Iterator)
res1 = []
for i in xrange(2):
for j in a:
res1.append(j)
b = TestB()
print isinstance(b, Iterable)
print isinstance(b, Iterator)
res2 = []
for i in xrange(2):
for j in b:
res2.append(j)
print res1
print res2
在上述代码中,
T
e
s
t
A
TestA
TestA是一个真正的迭代器对象,
T
e
s
t
B
TestB
TestB是一个可迭代对象,其
i
t
e
r
iter
iter方法会创建一个
T
e
s
t
A
TestA
TestA对象并返回。不过一般不太推荐这种写法,尤其在合作开发时,因为别人写的类可能会变动,从而导致错误。对于内置类型的话,使用这种方法还是比较方便的。
生成器
ok,接下来让我们看看 p y py py中的另外一个概念——生成器。据官方文档所言,生成器是一个用于创建迭代器的简单而强大的工具。 它们的写法类似于标准的函数,但当它们要返回数据时会使用 yield 语句。 每次在生成器上调用 next() 时,它会从上次离开的位置恢复执行(它会记住上次执行语句时的所有数据值)。
也就是说,生成器一定是迭代器。
话不多说,直接上代码:
# coding=utf-8
from collections import Iterable, Iterator
def testFunc():
lst = [1, 2, 3]
for ele in lst:
yield ele
def normalFunc():
pass
fun = testFunc()
print isinstance(fun, Iterator)
print isinstance(fun, Iterable)
print hasattr(fun, '__call__')
normal_fun = normalFunc
print isinstance(normal_fun, Iterator)
print isinstance(normal_fun, Iterable)
print hasattr(normal_fun, '__call__')
print next(fun)
print fun.next()
print fun.next()
print next(fun)
可以看到生成器和一般的函数还是有区别的:生成器实现了迭代器协议,但是没有实现
c
a
l
l
call
call方法,生成器的局部变量和执行状态会在每次调用之间自动保存,但普通函数每次都会重新开始执行。
再看一段代码:
# coding=utf-8
def testFunc():
lst = [1, 2, 3]
for ele in lst:
yield ele
print testFunc()
print testFunc()
print testFunc()
为什么结果是这样的?难道每次调用生成器函数返回的都是同一个对象吗?显然不是,出现这个结果的原因可能是内存局部性,因为上述输出语句创建了一个生成器对象,但是在语句结束后这个对象的引用计数就变成0了,于是它会被销毁掉,然后由于内存局部性,新创建的对象大概率还是会在原来的内存上。
我们可以测试一下:
ok,最后再简单的提一下生成器表达式。它和列表推导很像,只不过返回的结果是一个生成器,也因此更加节省内存:
# coding=utf-8
a = (i for i in xrange(5))
print a
print list(a)
不过我们依然要注意之前提到的小细节:
# coding=utf-8
lst = [1, 3, 5]
a = (ele for ele in lst)
print a
lst[1] = 10
print a
print list(a)
用闭包实现迭代器
我们还可以使用闭包来实现迭代器,这牵扯到 i t e r iter iter函数的第二种使用方式: i t e r ( c a l l a b l e , s e n t i n e l ) iter(callable,sentinel) iter(callable,sentinel)。当使用这种方式时,第一个参数必须是一个可调用对象,第二个参数是哨兵,也可以理解为终止符号。这种情况下生成的迭代器,每次迭代调用它的 n e x t ( ) next() next() 方法时都会不带实参地调用 c a l l a b l e callable callable;如果返回的结果是 s e n t i n e l sentinel sentinel 则触发 S t o p I t e r a t i o n StopIteration StopIteration,否则返回调用结果。
实现方式如下(运行环境为 p y t h o n 3 python3 python3):
# coding=utf-8
class Test(object):
def __init__(self, *data):
self.data = list(data)
def __iter__(self):
index = 0
def getElement():
ele = None
nonlocal index
if index < len(self.data):
ele = self.data[index]
index += 1
return ele
return iter(getElement, None)
t = Test(1, 2, 3)
for i in range(3):
res = [val for val in t]
print(res)
如果在
p
y
t
h
o
n
2
python2
python2中这段代码该如何实现呢?如果直接把运行环境改为
p
y
2
py2
py2,大概率会看到语法错误的提示:
其背后原理涉及到
p
y
t
h
o
n
python
python中的名空间和名空间查找规则。先来简单认识一下
p
y
py
py中的命名空间:
- 命名空间(Namespace)是从名称到对象的映射,大部分的命名空间都是通过 Python 字典来实现的。
- 命名空间提供了在项目中避免名字冲突的一种方法。各个命名空间是独立的,没有任何关系的,所以一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。
p y py py中的名空间可以分为以下几种类型:
- 局部名空间( l o c a l n a m e s local\ names local names),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。类中定义的同理。
- 全局名称( g l o b a l n a m e s global\ names global names),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
- 内置名称( b u i l t − i n n a m e s built-in\ names built−in names), P y t h o n Python Python 语言内置的名称,比如函数名 a b s 、 c h a r abs、char abs、char 和异常名称 B a s e E x c e p t i o n 、 E x c e p t i o n BaseException、Exception BaseException、Exception等等。
那么名空间查找规则会按照 L E G B LEGB LEGB的顺序进行,即: l o c a l − > e n c l o s i n g − > g l o b a l − > b u i l t − i n local->enclosing->global->built-in local−>enclosing−>global−>built−in。 e n c l o s i n g enclosing enclosing其实就是闭包外的函数的作用域,对于上述例子中的 g e t E l e m e n t getElement getElement来说,就是函数 i t e r iter iter的作用域。当然这个还取决于嵌套的层级,反正按照这个规则一层一层往上找就完事了。不过有一点需要注意,那就是写操作会打断向外查找的过程。因此在上述示例中, g e t E l e m e n t getElement getElement中的 i n d e x index index实际位于该函数的局部名空间,但是它在赋值前先被引用了,因此产生了错误。
有一个处理方式就是把 i n d e x index index定义为一个列表(只有一个元素),这样在闭包内修改第一个元素的值就不会报错了,不过代码看起来会很丑。