为何for循环能够遍历list:Python中迭代器与生成器

1 引言

只要你学了Python语言,就不会不知道for循环,也确定用for循环来遍历一个列表(list),那为何for循环能够遍历list,而不能遍历int类型对象呢?怎么让一个自定义的对象可遍历?shell

这篇博客中,咱们来一块儿探索一下这个问题,在这个过程当中,咱们会介绍到迭代器、可迭代对象、生成器,更进一步的,咱们会详细介绍他们的原理、异同。数据库

2 迭代器与可迭代对象

在开始下面内容以前,咱们先说说标题中的“迭代”一词。什么是迭代?我认为,迭代一个完整过程当中的一个重复,或者说每一次对过程的重复称为一次“迭代”,而每一次迭代获得的结果会做为下一次迭代的初始值,举一个类比来讲:一我的类家族的发展是一个完整过程,须要通过数代人的努力,每一代都会以接着上一代的成果继续发展,因此每一代都是迭代。网络

2.1 迭代器

(1)怎么判断是否可迭代app

做为一门设计语言,Python提供了许多必要的数据类型,例如基本数据类型int、bool、str,还有容器类型list、tuple、dict、set。这些类型当中,有些是可迭代的,有些不可迭代,怎么判断呢?函数

在Python中,咱们把全部能够迭代的对象统称为可迭代对象,有一个类专门与之对应:Iterable。因此,要判断一个类是否可迭代,只要判断是不是Iterable类的实例便可性能

>>> from collections import Iterable
>>> isinstance(123, Iterable)
False
>>> isinstance(True, Iterable)
False
>>> isinstance('abc', Iterable)
True
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance((), Iterable)
True

因此,整型、布尔不可迭代,字符串、列表、字典、元组可迭代。spa

怎么让一个对象可迭代呢?毕竟,不少时候,咱们须要用到的对象不止Python内置的这些数据类型,还有自定义的数据类型。答案就是实现__iter__()方法,只要一个对象定义了__iter__()方法,那么它就是可迭代对象。设计

from collections.abc import Iterable
class A():
    def __iter__(self):
        pass
print('A()是可迭代对象吗:',isinstance(A(),Iterable))

结果输出为:code

A()是可迭代对象吗: True对象

瞧,咱们在__iter__()方法里面甚至没写任何东西,反正咱们在类A中定义则__iter__()方法,那么,它就是一个可迭代对象。

重要的事情说3遍:

只要一个对象定义了__iter__()方法,那么它就是可迭代对象。

只要一个对象定义了__iter__()方法,那么它就是可迭代对象。

只要一个对象定义了__iter__()方法,那么它就是可迭代对象。

2.2 迭代器

迭代器是对可迭代对象的改造升级,上面说过,一个对象定义了__iter__()方法,那么它就是可迭代对象,进一步地,若是一个对象同时实现了__iter__()和__next()__()方法,那么它就是迭代器。

来,跟我读三遍:

若是一个对象同时实现了__iter__()和__next()__()方法,那么它就是迭代器。

若是一个对象同时实现了__iter__()和__next()__()方法,那么它就是迭代器。

若是一个对象同时实现了__iter__()和__next()__()方法,那么它就是迭代器。

在Python中,也有一个类与迭代器对应:Iterator。因此,要判断一个类是不是迭代器,只要判断是不是Iterator类的实例便可。

from collections.abc import Iterable
from collections.abc import Iterator
class B():
    def __iter__(self):
        pass
    def __next__(self):
        pass
print('B()是可迭代对象吗:',isinstance(B(), Iterable))
print('B()是迭代器吗:',isinstance(B(), Iterator))

结果输出以下:

B()是可迭代对象吗: True

B()是迭代器吗: True

可见,迭代器必定是可迭代对象,但可迭代对象不必定是迭代器。

因此整型、布尔必定不是迭代器,由于他们连可迭代对象都算不上。那么,字符串、列表、字典、元组是迭代器吗?猜猜!

>>> from collections.abc import Iterator
>>> isinstance('abc', Iterator)
False
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance((), Iterator)
False

惊不惊喜,意不意外,字符串、列表、字典、元组都不是迭代器。那为何它们能够在for循环中遍历呢?并且,我想,看到这里,就算你已经能够在形式上区分可迭代对象和迭代器,可是你可能会问,这有什么卵用吗?确实,没多少卵用,由于咱们还不知道__iter__()、__next__()究竟是个什么鬼东西。

接下来,咱们经过继续探究for循环的本质来解答这些问题。

2.3 for循环的本质

说到__iter__()和__next__()方法,就颇有必要介绍一下iter()和next()方法了。

(1)iter()与__iter__()

__iter__()的做用是返回一个迭代器,虽然上面说过,只要实现了__iter__()方法就是可迭代对象,可是,没有实现功能(返回迭代器)总归是有问题的,就像一个村长,当选以后,那就是村长了,可是若是尸位素餐不作事,那老是有问题的。

__iter__()方法毕竟是一个特殊方法,不适合直接调用,因此Python提供了iter()方法。iter()是Python提供的一个内置方法,能够不用导入,直接调用便可。

from collections.abc import Iterator
class A():
    def __iter__(self):
        print('A类的__iter__()方法被调用')
        return B()
class B():
    def __iter__(self):
        print('B类的__iter__()方法被调用')
        return self
    def __next__(self):
        pass
a = A()
print('对A类对象调用iter()方法前,a是迭代器吗:', isinstance(a, Iterator))
a1 = iter(a)
print('对A类对象调用iter()方法后,a1是迭代器吗:', isinstance(a1, Iterator))

b = B()
print('对B类对象调用iter()方法前,b是迭代器吗:', isinstance(b, Iterator))
b1 = iter(b)
print('对B类对象调用iter()方法后,b1是迭代器吗:', isinstance(b1, Iterator))

运行结果以下:

对A类对象调用iter()方法前,a是迭代器吗: False

A类的__iter__()方法被调用

对A类对象调用iter()方法后,a1是迭代器吗: True

对B类对象调用iter()方法前,b是迭代器吗: True

B类的__iter__()方法被调用

对B类对象调用iter()方法后,b1是迭代器吗: True

对于B类,由于B类自己就是迭代器,因此能够直接返回B类的实例,也就是说self,固然,你要是返回其余迭代器也没毛病。对于类A,它只是一个可迭代对象,__iter__()方法须要返回一个迭代器,因此返回了B类的实例,若是返回的不是一个迭代器,调用iter()方法时就会报如下错误:

TypeError: iter() returned non-iterator of type 'A'

(2)next()与__next__()

__next__()的做用是返回遍历过程当中的下一个元素,若是没有下一个元素则主动抛出StopIteration异常。而next()就是Python提供的一个用于调用__next__()方法的内置方法。

下面,咱们经过next()方法来遍历一个list:

>>> list_1 = [1, 2, 3]
>>> next(list_1)
Traceback (most recent call last):
File "<pyshell#19>", line 1, in <module>
next(list_1)
TypeError: 'list' object is not an iterator
>>> list_2 = iter(list_1)
>>> next(list_2)
1
>>> next(list_2)
2
>>> next(list_2)
3
>>> next(list_2)
Traceback (most recent call last):
File "<pyshell#24>", line 1, in <module>
next(list_2)
StopIteration

由于列表只是可迭代对象,不是迭代器,因此对list_1直接调用next()方法会产生异常。对list_1调用iter()后就能够得到是迭代器的list_2,对list_2每一次调用next()方法都会取出一个元素,当没有下一个元素时继续调用next()就抛出了StopIteration异常。

>>> class A():
      def __init__(self, lst):
          self.lst = lst
      def __iter__(self):
          print('A.__iter__()方法被调用')
          return B(self.lst)
>>> class B():
      def __init__(self, lst):
          self.lst = lst
          self.index = 0
      def __iter__(self):
          print('B.__iter__()方法被调用')
          return self
      def __next__(self):
          try:
              print('B.__next__()方法被调用')
              value = self.lst[self.index]
              self.index += 1
              return value
          except IndexError:
              raise StopIteration()
>>> a = A([1, 2, 3])
>>> a1 = iter(a)
A.__iter__()方法被调用
>>> next(a1)
B.__next__()方法被调用
1
>>> next(a1)
B.__next__()方法被调用
2
>>> next(a1)
B.__next__()方法被调用
3
>>> next(a1)
B.__next__()方法被调用
Traceback (most recent call last):
  File "<pyshell#78>", line 11, in __next__
    value = self.lst[self.index]
IndexError: list index out of range

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<pyshell#84>", line 1, in <module>
    next(a1)
  File "<pyshell#78>", line 15, in __next__
    raise StopIteration()
StopIteration

A类实例化出来的实例a只是可迭代对象,不是迭代器,调用iter()方法后,返回了一个B类的实例a1,每次对a1调用next()方法,都用调用B类的__next__()方法。

接下来,咱们用for循环遍历一下A类实例:

>>> for i in A([1, 2, 3]):
    print('for循环中取出值:',i)
 
A.__iter__()方法被调用
B.__next__()方法被调用
for循环中取出值: 1
B.__next__()方法被调用
for循环中取出值: 2
B.__next__()方法被调用
for循环中取出值: 3
B.__next__()方法被调用

经过for循环对一个可迭代对象进行迭代时,for循环内部机制会自动经过调用iter()方法执行可迭代对象内部定义的__iter__()方法来获取一个迭代器,而后一次又一次得迭代过程当中经过调用next()方法执行迭代器内部定义的__next__()方法获取下一个元素,当没有下一个元素时,for循环自动捕获并处理StopIteration异常。若是你还没明白,请看下面用while循环实现for循环功能,整个过程、原理都是同样的:

>>> a = A([1, 2, 3])
>>> a1 = iter(a)
A.__iter__()方法被调用
>>> while True:
    try:
      i = next(a1)
      print('for循环中取出值:', i)
    except StopIteration:
      break
 
B.__next__()方法被调用
for循环中取出值: 1
B.__next__()方法被调用
for循环中取出值: 2
B.__next__()方法被调用
for循环中取出值: 3
B.__next__()方法被调用
做为一个迭代器,B类对象也能够经过for循环来迭代:
>>> for i in B([1, 2, 3]):
    print('for循环中取出值:',i)
 
 
B.__iter__()方法被调用
B.__next__()方法被调用
for循环中取出值: 1
B.__next__()方法被调用
for循环中取出值: 2
B.__next__()方法被调用
for循环中取出值: 3
B.__next__()方法被调用
看出来了吗?这就是for循环的本质。

3 生成器

3.1 迭代器与生成器

若是一个函数体内部使用yield关键字,这个函数就称为生成器函数,生成器函数调用时产生的对象就是生成器。生成器是一个特殊的迭代器,在调用该生成器函数时,Python会自动在其内部添加__iter__()方法和__next__()方法。把生成器传给 next() 函数时, 生成器函数会向前继续执行, 执行到函数定义体中的下一个 yield 语句时, 返回产出的值, 并在函数定义体的当前位置暂停, 下一次经过next()方法执行生成器时,又从上一次暂停位置继续向下……,最终, 函数内的全部yield都执行完,若是继续经过yield调用生成器, 则会抛出StopIteration 异常——这一点与迭代器协议一致。

>>> from collections.abc import Iterable
>>> from collections.abc import Iterator
>>> def gen():
      print('第1次执行')
      yield 1
      print('第2次执行')
      yield 2
      print('第3次执行')
      yield 3

    
>>> g = gen()
>>> isinstance(g, Iterable)
True
>>> isinstance(g, Iterator)
True
>>> g
<generator object gen at 0x0000021CE9A39A98>
>>> next(g)
第1次执行
1
>>> next(g)
第2次执行
2
>>> next(g)
第3次执行
3
>>> next(g)
Traceback (most recent call last):
  File "<pyshell#120>", line 1, in <module>
    next(g)
StopIteration

能够看到,生成器的执行机制与迭代器是极其类似的,生成器本就是迭代器,只不过,有些特殊。那么,生成器特殊在哪呢?或者说,有了迭代器,为何还要用生成器?

从上面的介绍和代码中能够看出,生成器采用的是一种惰性计算机制,一次调用也只会产生一个值,它不会将全部的值一次性返回给你,你须要一个那就调用一次next()方法取一个值,这样作的好处是若是元素有不少(数以亿计甚至更多),若是用列表一次性返回全部元素,那么会消耗很大内存,若是咱们只是想要对全部元素依次一个一个取出来处理,那么,使用生成器就正好,一次返回一个,并不会占用太大内存。

举个例子,假设咱们如今要取1亿之内的全部偶数,若是用列表来实现,代码以下:

def fun_list():
    index = 1
    temp_list = []
    while index < 100000000:
        if index % 2 == 0:
            temp_list.append(index)
            print(index)
        index += 1
    return temp_list

上面程序会先获取全部符合要求的偶数,而后一次性返回。若是你运行了代码,你就会发现两个问题——运行时间很长、消耗不少内存。

有时候,咱们并不必定须要一次性得到全部的对象,须要一个使用一个就能够,这样的话,能够用生成器来实现:

>>> def fun_gen():
      index = 1
      while index < 100000000:
          if index % 2 == 0:
              yield index
          index += 1

        
>>> fun_gen()
<generator object fun_gen at 0x00000222DC2F4360>
>>> g = fun_gen()
>>> next(g)
2
>>> next(g)
4
>>> next(g)
6

看到了吗?对生成器没执行一次next()方法,就会返回一个元素,这样的话不管在速度上仍是机器性能消耗上都会好不少。若是你还没感觉到生成器的优点,我再说一个应用场景,假如须要取出远程数据库中的100万条记录进行处理,若是一次性获取全部记录,网络带宽、内存都会有很大消耗,可是若是使用生成器,就能够取一条,就在本地处理一条。

不过,生成器也有不足,正由于采用了惰性计算,你不会知道下一个元素是什么,更不会知道后面还有多少元素,因此,对于列表、元组等结构,咱们能调用len()方法获知长度,可是对于生成器却不能。

总结一下迭代器与生成器的异同:

(1)生成器是一种特殊的迭代器,拥有迭代器的全部特性;

(2)迭代器使用return返回值而生成器使用yield返回值每一次对生成器执行next()都会在yield处暂停;

(3)迭代器和生成器虽然都执行next()方法时返回下一个元素,迭代器在实例化前就已知全部元素,可是采用惰性计算机制,共有多少元素,下一个元素是什么都是未知的,每一次对生成器对象执行next()方法才会产生下一个元素。

3.2 生成器解析式

使用过列表解析式吗?语法格式为:[返回值 for 元素 in 可迭代对象 if 条件]

看下面代码:

>>> li = []
>>> for i in range(5):
      if i%2==0:
          li.append(i**2)

        
>>> li
[0, 4, 16]

咱们能够用列表解析式实现一样功能:

>>> li = [i**2 for i in range(5) if i%2==0]
>>> li
[0, 4, 16]
>>> type(li)
<class 'list'>

很简单对不对?简洁了不少,返回的li就是一个列表。咳咳……偏题了,咱们要说的是生成器解析式,并且我相信打开我这篇博文的同窗大多都熟悉列表解析式,回归正题。

生成器解析式语法格式为:(返回值 for 元素 in 可迭代对象 if 条件)

你没看错,跟列表解析式相比,生成器解析式只是把方括号换成了原括号。来感觉一下:

>>> g = (i**2 for i in range(5) if i%2==0)
>>> g
<generator object <genexpr> at 0x00000222DC2F4468>
>>> next(g)
0
>>> next(g)
4
>>> next(g)
16
>>> next(g)
Traceback (most recent call last):
File "<pyshell#38>", line 1, in <module>
next(g)
StopIteration

能够看到,生成器解析式返回的就是一个生成器对象,换句话说生成器解析式是生成器的一种定义方式,这种方式简单快捷,固然实现的功能不能太复杂。

4 总结

本文全面总结了Python中可迭代对象、迭代器、生成器知识,我相信,只要你认真消化我这篇博文,就能深入领悟迭代器生成器。

相关文章
相关标签/搜索