一文说清Python可迭代对象,迭代器,生成器的关系

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__()方法。

此迭代器内部的状态由precurr的实例变量来维持,并用于调用迭代器时产生的下一个序列。每次调用next(),会做两件事:

  1. 为下一次next()调用而修改状态
  2. 生产出当前调用的结果

从类的外部来看,迭代器相当于一个懒惰的工厂。它只有在需要值的时候才生产出值。当生产出一个值后,将停止生产直到你下一次调用它。这就是懒加载。

生成器

生成器可以算得上是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]

代码说明:

  1. fib是一个普通的Python函数,但它没有包含return关键字。
  2. fib的函数返回值是一个生成器(迭代器,工厂函数)
  3. 当f = fib() 被调用,生成器被实例化并返回。此时并没有执行任何代码,也就是说prev,curr = 0, 1并没有执行。
  4. 生成器实例被islice()包装,这也是一个迭代器,因此最初时是空闲状态,没有任何事情发生。
  5. 然后,迭代器被list()包装,它是一个消费者,消费它所包含的参数并从中构建成 一个列表的形式。此时,将在islice()实例中调用next()方法,并在f中调用next()方法。
  6. 此时,代码开始真正执行,进入到循环中,直到遇到yield,产生一个值后,将再次进入空闲状态。
  7. 产生的值传递给islice(),并生产出来。list增加值1到列表中。
  8. 然后循环往复,直到输出列表的长度为10个元素
  9. 求第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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值