generator、iterable、iterator傻傻分不清楚
通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器(Generator)。
下面介绍两种generator生成方式。
表达式形式 | (x for x in range(10)) | |
函数形式 | def gen(): yield 1 | |
一、简单生成器(推导式)
>>> L = [x for x in range(10)]
>>> L2 = (x for x in range(12))
>>> print(L)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> print(L2)
<generator object <genexpr> at 0x01C8A4C0>
生成器对象创建与列表推导式不同的地方就是,生成器推导式是用圆括号创建。
遍历方式
根据《设计模式:可复用面向对象软件的基础》一书的定义,迭代器用于从集合中取出元素;而生成器用于“凭空”生成元素。通过斐波那契数列能很好说明二者之间的区别:斐波那契数列的数有无穷个,在一个集合里放不下。
真正需要计算出值的时候才会去往下计算出值。它是一种惰性计算(lazy evaluation)。遍历需要使用next()或generator.__next__()
方法。
>>> print(L2.__next__())
0
>>> print(next(L2))
1
也可以使用for循环遍历,generator也属于可迭代对象iterable。后面我们会深入探讨一下迭代对象。
所有生成器都是迭代器,因为生成器完全实现了迭代器接口。在Python社区中,大多数时候都把迭代器和生成器视作同一概念。
for i in L2:
print(i)
但绝大多数情况下,都不会使用next()方式,因为当遍历完最后一个元素时,再使用next()方法会弹出异常StopIteration:
,而且,我们应该尽量避免直接调用python内置的特殊方法。
二、带yield语句的Function
Python没有宏,因此为了抽象出迭代器模式,需要改动语言本身。为此,Python2.2(2001年)加入了yield关键字,这个关键字用于构建生成器(generator),其作用与迭代器一样。
def fib2(maxValue):
n, a, b = 0, 0, 1
while n < maxValue:
yield b
a, b = b, a + b
n = n + 1
f = fib2(4)
print(f)
<generator object fib2 at 0x7f15d41148d0>
如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:
**注意:**带yield语句的函数和正常函数的执行顺序不一致,准确的说,正常的函数返回type为<class 'NoneType'>
。带yield
语句的函数返回类型为<class 'generator'>
。
- 正常函数执行是顺序执行
- 带
yield
的函数执行是当调用next()
时执行,执行完yield
语句块后返回,再次执行时从上次返回的yield语句的下一句开始。 - 注意:yield语句的功能是返回值,相当于
return
的作用,通过next(generator)可以直接返回yield值
def f():
print(0)
yield 1
print(2)
yield 3
print(4)
>>> f = f() #此时不会执行函数内部语句
>>> next(f)
0
1
>>> next(f)
2
3
特殊特性
send():向yield变量传入值。可以是字符串、数字类型
-
- 补充python数据结构的相关知识
def foo():
print('starting')
while True:
r = yield 2
print(r)
f = foo()
print(f.send(None)) #与print(next(f))效果一致
print(f.send(1))
>>> starting
>>> 2
>>> 1
>>> 2
f.send(None)
:效果等同于next(f),此时函数输出了‘starting’,然后执行了yield 2,即返回2。所以print(f.send(None))
的结果是
>>> starting
>>> 2
print(f.send(1))
:将1传给yield,yield把值赋给r
。随后打印r
。最后通过循环回到yield 2
再返回2
r = yield 2 主要分两步:
第一步: yield 2 ,也就是先返回2
第二步: r = (yield) 这里用括号把yield包起来是为了突出yield是一个表达式expression:可以用来表示某个值。
三、迭代器
在正式引入可迭代对象和迭代器概念前,我想首先说明一下**“迭代”的概念。这并不是一个很高深莫测的概念,事实上,当你熟悉了某一门编程语言后,经常会使用循环结构来完成一些任务,大多数高级语言中都有for循环的定义。而一个比“循环”稍专业一些的词汇就是“遍历”或者“迭代”**。它们均表达了我们要使用循环结构去一个元素一个元素的使用某一个对象。
3.1 单词序列第1版
我们需要实现一个Sentence类,以此打开探索可迭代对象。通过类的构造方法传入一个文本序列,然后通过for循环实现这个文本序列的迭代。通过这个例子说明为什么可以迭代
class Sentence:
def __init__(self, text):
self.text = text
self.words = RE_WORD.findall(text) # re.findall函数返回一个字符串列表,里面的元素是正则表达式的全部非重叠匹配
# self.words中保存的是.findall函数返回的结果,因此直接返回指定索引位上的单词
def __getitem__(self, index):
return self.words[index]
def __len__(self):
return len(self.words)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text) # reprlib.repr用于生成大型数据结构的简略字符串表示形式。
# 默认情况下,reprlib.repr生成的字符串最多有30个字符。
re.findall
函数返回一个字符串列表,里面的元素是正则表达式的全部非重叠匹配self.words
中保存的是.findall
函数返回的结果,因此直接返回指定索引位上的单词- 为了完善序列协议,我们实现了
__len__
方法;不过,为了让对象可以迭代,没必要实现这个方法。 reprlib.repr
用于生成大型数据结构的简略字符串表示形式。默认情况下,reprlib.repr
生成的字符串最多有30个字符。
通过for循环测试Sentence实例能否迭代。
>>> s = Sentence('"The time has come," the Walrus said,')
>>> for word in s:
... print(word)
print(s[0]) #此时的Sentence类可以按照索引获取单词。
The
time
has
come
the
Walrus
said
The
3.1.1序列可以迭代的原因:iter函数
Python的内置函数iter
有以下作用。
- 检查对象所属类的内部是否实现了
__iter__
方法,如果实现了就调用它,返回一个迭代器。 - 如果没有实现
__iter__
方法,但是实现了__getitem__
方法,Python会创建一个迭代器,尝试按顺序(从索引为0开始)获取元素。 - 如果尝试失败,Python抛出TypeError异常,通常会提示“C object is not iterable”(C对象不可迭代),其中C是目标对象所属的类。
任何Python序列都可迭代的原因是,它们都实现了
__getitem__
方法。其实,标准的序列也都实现了__iter__
方法,因此你也应该这么做。之所以对__getitem__
方法做特殊处理,是为了向后兼容,而未来可能不会再这么做。这是鸭子类型(duck typing)的极端形式:不仅要实现特殊的
__iter__
方法,还要实现__getitem__
方法,而且__getitem__
方法的参数是从0开始的整数(int),这样才认为对象是可迭代的。在白鹅类型(goose-typing)理论中,可迭代对象的定义简单一些,不过没那么灵活:如果实现了
__iter__
方法,那么就认为对象是可迭代的。此时,不需要创建子类,也不用注册,因为abc.Iterable
类实现了__subclasshook__
方法。
>>>class Foo:
... def __iter__(self):
... pass
>>>f = Foo()
>>>print(issubclass(Foo, abc.Iterable))
>>>print(issubclass(type(f), abc.Iterable))
True
True
注意:虽然Sentence类是可以迭代的,但却无法通过issubclass(Sentence,abc.Iterable)
测试。
3.1.2可迭代对象
- 实现了能返回迭代器的
__iter__
方法 - 序列都可以迭代
- 实现了
__getitem__
方法,而且其参数是从零开始的索引。
从Python3.4开始,检查对象x能否迭代,最准确的方法是:调用iter(x)函数,如果不可迭代,会抛出TypeError异常。这比使用
issubclass(x,abc.Iterable)
更准确,因为Iter(x)函数会考虑遗留的__getitem__
方法,而abc.Iterable
类则不会考虑。
>>>print(iter(Sentence("s")))
<iterator object at 0x021B1F40>
>>>class Foo:
... pass
>>>print(iter(Foo()))
TypeError: 'Foo' object is not iterable
3.1.3迭代器
迭代器是从可迭代对象中获取的。对于一个字符串序列,它是一个可迭代对象,通过issubclass
和iter
均可进行验证。同时可以从字符串对象中获取字符串的迭代器。
>>>from collections import abc
>>>s = "abc"
>>>print(issubclass(type(s),abc.Iterable))
>>>print(iter(s))
True
<str_iterator object at 0x016CF6E8>
接下来通过iter
获取其迭代器对象。
>>>it = iter(s)
>>>while True:
... try:
... print(next(it))
... except StopIteration: # 如果没有字符,迭代器会抛出StopIteration异常
... del it #释放对it的引用,即废弃迭代器对象
... break
a
b
c
Python源码中Iterable与Iterator的UML类图如下:
Iterable和Iterator是抽象类,以斜体显示的是抽象方法。
标准的迭代器接口有两个方法。
__next__
:返回下一个可用的元素,如果没有元素,抛出StopIteration异常__iter__
:返回self,即返回迭代器对象本身。以便在应该使用可迭代对象的地方使用迭代器,例如在for循环中。
在Python3.6中,_collections_abc.py中关与Iterator的类的源码如下。源码在线阅读
class Iterator(Iterable):
__slots__ = ()
@abstractmethod
def __next__(self):
'Return the next item from the iterator. When exhausted, raise StopIteration'
raise StopIteration
def __iter__(self):
return self
@classmethod
def __subclasshook__(cls, C):
if cls is Iterator:
return _check_methods(C, '__iter__', '__next__')
return NotImplemented
在Python 3.6中,Lib/types.py模块的源码里有如下注释:
# Iterators in Python aren't a matter of type but of protocol. A large
# and changing number of builtin types implement *some* flavor of
# iterator. Don't check the type! Use hasattr to check for both
# "__iter__" and "__next__" attributes instead.
上面的注释是abc.Iterator抽象类中__subclassshook__
方法的作用。考虑到源码中的建议,检查对象x是否为迭代器最好的方式是调用isinstance(x,abc.Iterator)。得益于**Iterator.__subclassshook__
方法,即使对象x所属的类不是Iterator**类的真实子类或虚拟子类,也能这样检查。
从源码中可以得出迭代器的定义如下:
迭代器是这样的对象:
- 实现无参数的
__next__
方法,返回序列中的下一个元素;如果没有元素了,那么抛出StopIteration异常。 - 实现了
__iter__
方法,因此迭代器也可以迭代。
但我们的Sentence类并没有实现上面的两个方法,为什么可以通过Python内置的iter()
方法返回Sentence对象的迭代器呢?其实上面介绍iter()
方法时已经讲过,因为Sentence类实现了__getitem__
方法,实现了此方法也可以通过python内置的iter()
方法返回迭代器。
3.2 单词序列第2版
第2版的Sentence类根据《设计模式:可复用面向对象软件的基础》一书给出的模型,实现典型的迭代器设计模式。
class SentenceIterator:
def __init__(self,words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index]
except IndexError:
raise StopIteration()
self.index += 1
return word
def __iter__(self):
return self
class Sentence:
def __init__(self,text):
self.text = text
self.words = RE_WORD.findall(text)
def __repr__(self):
return 'Sentence(%s)' % reprlib.repr(self.text)
def __iter__(self):
return SentenceIterator(self.words)
第二版的Sentence类相比第一版,删去了__getitem__
,新增了__iter__
方法。为了更好说明可迭代对象的特征。SentenceIterator
是Sentence返回的迭代器对象,其主要是处理迭代器的内部状态。可以for循环检验Sentence是否可迭代。
>>>s = Sentence('hello world')
>>>for i in s: #这行代码执行了__iter__(),并返回了SentenceIterator对象
... print(i)
hello
world
读者在这里一定觉得**SentenceIterator
很多余,为什么不能直接在Sentence
**类中实现__next__
方法。让Sentence既是可迭代对象,又是自身的迭代器。但现实中,这种想法很糟糕,这是常见的反模式。
《设计模式:可复用面向对象软件的基础》一书讲解迭代器设计模式时,在“适用性”一节中说(P172):
迭代器模式可用来:
- 访问一个聚合对象的内容而无需暴露它的内部表示
- 支持对聚合对象的多种遍历
- 为遍历不同的聚合结构提供一个统一的接口(多态迭代)
为了“支持多种遍历”,必须能从同一个可迭代的实例中获取多个独立的迭代器,而且各个迭代器要能维护自身的内部状态。所以每次必须通过**__iter__
创建SentenceIterator
**迭代器实例。
上面是书中的定义,我用通俗易懂的语言来解释一下。在一个py文件中,针对一个Sentence实例,可能会多出遍历它,但多数场景中,我们希望每次遍历的Sentence对象都是一个完整的、新的对象。而不是接着上次遍历到的地方继续遍历。看一个正确的例子。
>>>s = Sentence('hello world i am lgr')
>>>for i in s:
... print(i)
... break
>>>for i in s:
... print(i)
hello
hello
world
i
am
lgr
可以看到,第二次遍历对象,是重新开始遍历,没有从上次遍历结束的位置开始。如果是Sentence自己来做迭代器,那就会从上次遍历结束的位置开始遍历。
总结:
可迭代的对象一定不能是自身的迭代器。即可迭代对象必须实现**__iter__
方法,但不能实现__next__
方法。另一方面,迭代器应该一直可以迭代。迭代器的__iter__
**方法应该返回自身。