Python的初学者可能会对以下概念感到困惑:
- 容器
- 可迭代对象
- 迭代器
- 生成器
- 生成器表达式
这篇文章将有助于加深对上述概念的理解,并梳理它们之间的异同之处。
容器
容器是一种数据结构,它可收纳元素,并支持成员关系判断。它们是存储在内存中的数据结构,通常在内存中维持着元素。在Python中,它们包括:
- list , deque,…
- set , fronzensets,…
- dict , defaultdict, OrderedDict, Counter,…
- str
此处的容器,跟日常生活中容器的概念是相似的,比如一个盒子,一间房子,一艘船等。
技术上而言,当询问一个对象是否包含某个元素时,该对象就是容器。因此,可将诸如成员关系判断的方法,应用于容器中。
比如:list、sets、tuples:
>>> assert 1 in [1, 2, 3] # lists
>>> assert 4 not in [1, 2, 3]
>>> assert 1 in {1, 2, 3} # sets
>>> assert 4 not in {1, 2, 3}
>>> assert 1 in (1, 2, 3) # tuples
>>> assert 4 not in (1, 2, 3)
而dict的成员关系判断,则是判断key:
>>> d = {1: 'foo', 2: 'bar', 3: 'qux'}
>>> assert 1 in d
>>> assert 4 not in d
>>> assert 'foo' not in d # 'foo' is not a _key_ in the dict
如果判断dict的value,则可使用:
>>> assert 'foo' in d.values()
而字符串str,也是包含着元素,支持成员关系判断。因此它也属于容器:
>>> s = 'foobar'
>>> assert 'b' in s
>>> assert 'x' not in s
>>> assert 'foo' in s # a string "contains" all its substrings
实际上,当使用成员关系判断时,是调用了该对象的类方法__contains__
,如果没有实现此方法,则调用对象的类方法__iter__
。也就是说,大多数容器对象,都提供了这两种方法或其一。
尽管大多数容器提供了一种迭代出每个元素的能力,但并非只有容器才具有这种能力。准确而言,具有这种能力(迭代出元素)的对象,称为可迭代对象,也就是说,实现了__iter__
方法的对象,为可迭代对象。
通常而言,大多数容器是可迭代对象。
可通过下面例子加深理解:
-
是容器,支持成员关系判断,但不是可迭代对象:
class Foo: def __init__(self,item): self.item = item def __contains__(self, *args): print("membership testing...") return args[0] in self.item f = Foo([1,2,3,4]) print(1 in f) # 成员关系判断,调用了__containers__方法 print(100 in f) # 同上 --- for ele in f: # 迭代,调用了__iter__方法。因为没有定义,因此是不可迭代对象 print(ele)
输出:
membership testing... True membership testing... False -- Traceback (most recent call last): File "E:/PyProject/test02.py", line 14, in <module> for ele in f: TypeError: 'Foo' object is not iterable
-
是容器,支持成员关系判断,是可迭代对象:
class Foo: def __init__(self,item): self.item = item self.iter = iter(item) def __contains__(self, *args): print("membership testing...") return args[0] in self.item def __iter__(self): print('Ready to iter...') return self.iter f = Foo([1,2,3,4]) for ele in f: # 是可迭代对象,因此可迭代 print(ele)
输出:
Ready to iter... 1 2 3 4
容器中不是可迭代对象的类,比如Bloom filter。虽然Bloom filter可以用来检测某个元素是否包含在容器中,但是并不能从容器中获取其中的每一个值,也就是无法迭代。
因为Bloom filter压根就没把元素存储在容器中,而是通过一个散列函数映射成一个值保存在数组中。
可迭代对象
如上文所述,大多数容器都是可迭代对象。但不仅于此,如打开的文件,打开数sockets也是可迭代对象。
通常情况下,容器所包含的数据是有限的。而可迭代对象却可以是无穷的数据源。
一个可迭代对象可以是任何一种对象,并不仅限于是一种数据结构。只要提供了__iter__
方法的对象,都是可迭代对象。
一般来说,能够返回一个迭代器的对象,都是可迭代对象。因为没有人定义了一个迭代器,但却无法迭代。
迭代器的目标,就是返回它所包含的全部元素。
比如下面的例子:
>>> x = [1, 2, 3] # 可迭代对象
>>> y = iter(x)
>>> z = iter(x) # 返回了迭代器对象
>>> next(y)
1
>>> next(y)
2
>>> next(z)
1
>>> type(x)
<class 'list'>
>>> type(y)
<class 'list_iterator'>
此处,x是可迭代对象,而y和z是两个独立的迭代器实例,可以从可迭代对象x中生产出元素。可以使用内建函数next(iterator,[default])
来判断一个对象是否是迭代器。
y和z会维持一种状态。该状态用于记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。
总结:
如果一个可迭代对象的类支持__iter__()
和__next__()
方法,并且__iter__()
返回self,这会使得类既是一个可迭代对象,也是自己的迭代器。不过,最好返回一个与迭代器不同的对象。
也就是说,迭代时,会调用__iter__
方法,返回一个新的迭代器对象。当然也可以自定义返回的对象。
最后,迭代元素时:
x = [1, 2, 3]
for elem in x:
...
实际上的过程是:
当你反编译这段Python代码时,你会看到解释器显式地调用GET_ITER
,这本质上就是调用iter(x)
。FOR_ITER
指令会调用等效于next()
去重复获取每个元素,但这并没有在字节码指令中显示出来,因为这被解释器优化过了。
>>> import dis
>>> x = [1, 2, 3]
>>> dis.dis('for _ in x: pass')
1 0 SETUP_LOOP 14 (to 17)
3 LOAD_NAME 0 (x)
6 GET_ITER
>> 7 FOR_ITER 6 (to 16)
10 STORE_NAME 1 (_)
13 JUMP_ABSOLUTE 7
>> 16 POP_BLOCK
>> 17 LOAD_CONST 0 (None)
20 RETURN_VALUE
迭代器
那什么是迭代器呢?它是一个对象,当你调用next()
时会有效地迭代出(生产出)一个值。
任何实现了__next__()
方法的对象,都是一个迭代器。因此,迭代器是一个生产值的工厂。每次你询问下一个值时,它将会知道如何计算此值,因为它维持了内部的状态。
有很多关于迭代器的例子,所有itertools
函数都返回迭代器,比如生成无限的序列:
>>> from itertools import count
>>> counter = count(start=13)
>>> next(counter)
13
>>> next(counter)
14
比如从有限序列中循环返回序列:
>>> from itertools import cycle
>>> colors = cycle(['red', 'white', 'blue'])
>>> next(colors)
'red'
>>> next(colors)
'white'
>>> next(colors)
'blue'
>>> next(colors)
'red'
为了更直观地理解迭代器的内部执行过程,我们定义了一个产生斐波那契数列的迭代器
>>> class fib:
... def __init__(self):
... self.prev = 0
... self.curr = 1
...
... def __iter__(self):
... return self
...
... def __next__(self):
... value = self.curr
... self.curr += self.prev
... self.prev = value
... return value
...
>>> f = fib()
>>> list(islice(f, 0, 10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
需要注意的是,这个类既是可迭代对象,也是迭代器。因为它实现了__iter__()
和__next__()
方法。
此迭代器内部的状态由pre
和curr
的实例变量来维持,并用于调用迭代器时产生的下一个序列。每次调用next()
,会做两件事:
- 为下一次
next()
调用而修改状态 - 生产出当前调用的结果
从类的外部来看,迭代器相当于一个懒惰的工厂。它只有在需要值的时候才生产出值。当生产出一个值后,将停止生产直到你下一次调用它。这就是懒加载。
生成器
生成器可以算得上是Python中最吸引人的语言特性之一,它实际上是一种特殊的迭代器,但更加优雅。
一个生成器允许你编写一个类似于斐波那契数列的迭代器,但它简洁优雅的语法允许你无需提供__iter__()
和__next__()
方法。
可以理解为,使用yield
关键字的函数,就是生成器函数。
因此可以概括为:
- 生成器也是迭代器。
- 生成器是一个懒加载的工厂函数。
下面的代码使用生成器实现了斐波那契数列的工厂函数:
>>> def fib():
... prev, curr = 0, 1
... while True:
... yield curr
... prev, curr = curr, prev + curr
...
>>> f = fib()
>>> list(islice(f, 0, 10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
代码说明:
- fib是一个普通的Python函数,但它没有包含return关键字。
- fib的函数返回值是一个生成器(迭代器,工厂函数)
- 当f = fib() 被调用,生成器被实例化并返回。此时并没有执行任何代码,也就是说
prev,curr = 0, 1
并没有执行。 - 生成器实例被
islice()
包装,这也是一个迭代器,因此最初时是空闲状态,没有任何事情发生。 - 然后,迭代器被
list()
包装,它是一个消费者,消费它所包含的参数并从中构建成 一个列表的形式。此时,将在islice()
实例中调用next()
方法,并在f中调用next()方法。 - 此时,代码开始真正执行,进入到循环中,直到遇到yield,产生一个值后,将再次进入空闲状态。
- 产生的值传递给
islice()
,并生产出来。list增加值1到列表中。 - 然后循环往复,直到输出列表的长度为10个元素
- 求第11个值时,
islice()
将引发StopIteration
异常,表明已到达末尾,并且list将返回结果:list 10个项。其中包含前10个斐波那契数。 请注意,生成器没有收到第11个next()
调用。 实际上,它不会再次使用,以后会被垃圾回收。
生成器是程序结构中非常有效的工具,它允许你使用很少的中间变量和数据结构来编写流式代码。并且,它们在CPU和内存的表现中更为高效,代码更为简洁。
但凡看到下面的代码,都可用生成器重构:
def something():
result = []
for ... in ...:
result.append(x)
return result
生成器:
def iter_something():
for ... in ...:
yield x
# def something(): # Only if you really need a list structure
# return list(iter_something())
生成器表达式
在Python中的生成器有两类:生成器函数和生成器表达式。
生成器函数是拥有yield
关键字的函数。
生成器表达式是列表解析式的生成器版本,看起来像列表解析式,但是它返回的是一个生成器对象而不是列表对象。
比如列表解析式,集合解析式和字典解析式:
>>> numbers = [1, 2, 3, 4, 5, 6]
>>> [x * x for x in numbers]
[1, 4, 9, 16, 25, 36]
>>> {x * x for x in numbers}
{1, 4, 36, 9, 16, 25}
>>> {x: x * x for x in numbers}
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36}
生成器表达式:
>>> a = (x*x for x in range(10))
>>> a
<generator object <genexpr> at 0x401f08>
>>> sum(a) ## 已经消费完a里面的值
285
>>> a = (x*x for x in range(10))
>>> next(a) ## 开始消费
0
>>> list(a)
[1,2,3,4,5,6,7,8,9]
总结:
在实际使用过程汇总,如果需要延迟计算,懒加载的场景,使用生成器更加节省内存资源,而且CPU利用效率更高。
参考:https://nvie.com/posts/iterators-vs-generators/