11.【Python编程】迭代器与生成器

11.【Python编程】迭代器与生成器

备注: 本教程主要使用Python3.6在jupyter notebook上编程实现。Python环境配置参考《【Python学习】Windows10开始你的Anaconda安装与Python环境管理》或者《【Python学习】纯终端命令开始你的Anaconda安装与Python环境管理》

11.1 迭代的概念

  • 迭代的概念
    其实,我们在小学、初中、高中,都有接触迭代的概念。迭代是重复反馈过程的活动,每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。典型的迭代案例就是高中的数列知识,例如:等差数列、等比数列。
    等差数列:
    a n + 1 − a n = d , n ≥ 1 a_{n+1}-a_{n}=d, n\geq1 an+1an=d,n1
    等比数列:
    a n + 1 a n = q , n ≥ 1 \frac{a_{n+1}}{a_{n}}=q, n\geq1 anan+1=q,n1
    高中课本就指出了,数列的表示方法有图像法、列表法、通项公式法、递推公式法。其中,通项公式法、递推公式法不需要存储数列的每一项,只需要存储第一项 a 1 a_1 a1,以及对应的迭代公式(通项公式、递推公式)就可以了。上述操作可以用for循环表示出来。
    在Python当中,使用for循环遍历取值的过程叫做迭代。

  • 迭代的优点
    迭代是数据处理的基石,扫描内存中放不下的数据时,我们需要找到一种惰性获取数据项的方式,即按需一次获取一个数据项。这就是迭代器模式。

# 等差数列示例
a_1 = 1
d = 2
# 输出等差数列的前10项
n = 10
print("i",":","a_i")
for i in range(n):
    print(i+1,":",a_1+i*d)
i : a_i
1 : 1
2 : 3
3 : 5
4 : 7
5 : 9
6 : 11
7 : 13
8 : 15
9 : 17
10 : 19

11.2 可迭代对象

列表、元组、字典、集合、字符串、range对象都是可迭代对象。我们可以使用isinstance()判断一个对象是否是可迭代对象。
常见可迭代对象的迭代测试如下:

print("列表执行迭代操作测试")
list1 = [1,2,3,4]
for i in list1:
    print(i)
print("字典执行迭代操作测试")
dict1 = {"一":1,"二":2,"三":3}
for i in dict1:
    print(i)
print("元组执行迭代操作测试")    
tuple1 = ("a","b","c","d")
for i in tuple1:
    print(i)
print("集合执行迭代操作测试")    
set1 = {"a","b","c","d"}
for i in set1:
    print(i)
print("字符串执行迭代操作测试") 
str1 = "Hello"
for i in str1:
    print(i)    
print("range对象执行迭代操作测试") 
for i in range(5):
    print(i)     
列表执行迭代操作测试
1
2
3
4
字典执行迭代操作测试
一
二
三
元组执行迭代操作测试
a
b
c
d
集合执行迭代操作测试
d
a
c
b
字符串执行迭代操作测试
H
e
l
l
o
range对象执行迭代操作测试
0
1
2
3
4

使用isinstance()判断一个对象是否是可迭代对象,方式如下:

from collections import Iterable
isinstance(<待判断的对象>,Iterable)

返回True说明是可迭代对象,否则是不可迭代对象

from collections import Iterable
list1 = [1,2,3,4]
print("list1是可迭代对象?", isinstance(list1,Iterable))
num1 = 1000
print("num1是可迭代对象?", isinstance(num1,Iterable))
list1是可迭代对象? True
num1是可迭代对象? False

11.3 迭代器的定义

  • 迭代器的定义
    迭代器就是用于迭代操作(for循环)的对象,它像列表一样可以迭代获取其中的每一个元素,任何实现了 __next__方法(python2 是next)的对象都可以称为迭代器。
    它与列表的区别在于,构建迭代器的时候,不像列表把所有元素一次性加载到内存,而是以一种延迟计算(lazy evaluation)方式返回元素,这正是它的优点。比如列表含有中一千万个整数,需要占超过400M的内存,而迭代器只需要几十个字节的空间。因为它并没有把所有元素装载到内存中,而是等到调用 next 方法时候才返回该元素(按需调用 call by need 的方式,本质上 for 循环就是不断地调用迭代器的next方法)。

  • 迭代器的特性

    • 迭代器是一个可以记住遍历的位置的对象。
    • 迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
    • 把一个类作为一个迭代器使用需要在类中实现两个方法 __iter__()__next__()
  • Python的类写成迭代器形式

class ClassName:
    def __init__(self):
        # 类的构造方法
    def __iter__(self):
        # 返回self,以便在应该使用可迭代对象的地方使用迭代器
        # 类似于数列的首项
        self.a = 1
        return self
 
    def __next__(self):
        # 返回下一个元素
        # 类似于数列的递推公式
        x = self.a
        self.a += 1
        return x

因为迭代器只需__next____iter__两个方法,所以除了调用next()方法,以及捕获StopIteration异常之外,没有办法检查是否还有遗留的元素。此外,也没有办法“还原”迭代器。如果想再次迭代,那就要调用iter(...)函数,传入之前构建迭代器的可迭代对象。传入迭代器本身没用,因为Iterator.__iter__方法的实现方式是返回实例本身,所以传入迭代器无法还原已经耗尽的迭代器。

例1:使用迭代器构建等差数列(AP, Arithmetic progression)类

class AP:
    def __init__(self,n=10, a1=1, d=1):
        self.a_i = a1 # 等差数列首项
        self.d = d # 等差数列的公差
        self.n = n # 需要迭代多少项
        self.i = 1 # 数列的下标
    def __iter__(self):
        return self
    def __next__(self):
        # 输出a_i且更新a_{i+1}
        if self.i <= self.n:
            value = self.a_i
            self.a_i = self.a_i + self.d # 等差数列递推公式
            self.i = self.i + 1 # 更新数列的下标
            return value
        else:
            raise StopIteration() # StopIteration异常表明迭代器到头了

使用AP类构建首项为1,公差为3的等差数列对象AP1,输出前6项

AP1 = AP(n=6,a1=1,d=3)
for i in AP1:
    print(i)
1
4
7
10
13
16
# 上一个cell已经迭代完了,这里再次执行迭代,不会有任何输出
for i in AP1:
    print(i)

例2:使用迭代器构建等比数列(GP, Geometric Progression)类

class GP:
    def __init__(self,n=10, a1=1, q=2):
        self.a_i = a1 # 等比数列首项
        self.q = q # 等比数列的公比
        self.n = n # 需要迭代多少项
        self.i = 1 # 数列的下标
    def __iter__(self):
        return self
    def __next__(self):
        # 输出a_i且更新a_{i+1}
        if self.i <= self.n:
            value = self.a_i
            self.a_i = self.a_i*self.q # 等比数列递推公式
            self.i = self.i + 1 # 更新数列的下标
            return value
        else:
            raise StopIteration() # StopIteration异常表明迭代器到头了

使用GP类构建首项为1,公比为2的等比数列对象GP1,输出前6项

GP1 = GP(n=6,a1=1,q=2)
for i in GP1:
    print(i)
1
2
4
8
16
32

11.4 iter()函数与next()函数

我们可以通过iter()函数获取这些可迭代对象的迭代器。然后我们可以对获取到的迭代器不断使用next()函数来获取下一条数据。iter()函数实际上就是调用了可迭代对象的__iter__方法。
当我们已经迭代完最后一个数据之后,再次调用next()函数会抛出StopIteration的异常,来告诉我们所有数据都已迭代完成,不能再执行next()函数了。

list3 = [1,2,3,4,5,6,7,8]
list3_iter = iter(list3) # 创建迭代器对象
print("使用next函数进行迭代")
print(next(list3_iter))
print(next(list3_iter))
print(next(list3_iter))
print("剩余的使用for循环进行迭代")
for i in list3_iter:
    print(i)

'''
print(next(list3_iter))
报错
StopIteration:
'''
使用next函数进行迭代
1
2
3
剩余的使用for循环进行迭代
4
5
6
7
8





'\nprint(next(list3_iter))\n报错\nStopIteration:\n'

11.5 生成器的定义

在 Python 中,使用了关键字yield的函数被称为生成器(generator)。跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。在调用生成器运行的过程中,每次遇到yield时函数会暂停并保存当前所有的运行信息,返回yield的值, 并在下一次执行next()方法时从当前位置继续运行。调用一个生成器函数,返回的是一个迭代器对象。

  • 创建生成器的方法1:生成器表达式
    第一种⽅法很简单,只要把一个列表生成式的[ ]改成( )
L = [i**3 for i in range(6)] # 列表类型
G = (i**3 for i in range(6)) # 生成器类型
print(L)
print(type(L))
print(G)
print(type(G))
[0, 1, 8, 27, 64, 125]
<class 'list'>
<generator object <genexpr> at 0x7fe7e02780f8>
<class 'generator'>
  • 创建生成器的方法2:yield关键字
def function_name(参数):
    # 处理过程
    ...
    yield [返回结果]

使用生成器实现斐波那契数列,如下:

def fibonacci(n):
    a, b, counter = 0, 1, 0
    while True:
        if (counter >= n): 
            return
        yield a
        a, b = b, a + b
        counter += 1
# 打印斐波那契数列的前10项
for i in fibonacci(10):
    print(i)
0
1
1
2
3
5
8
13
21
34

11.6 生成器与迭代器的语义对比

三个方面:
1.接口

python迭代器协议定义了两个方法__next__和__iter__。生成器对象实现了这两个方法,因此,从这方面来看,所有的生成器都是迭代器。

2.实现方式

生成器这种python语言结构可以用两种方式编写:含有yield关键字的函数,或者生成器表达式。调用生成器函数或者执行生成器表达式得到的生成器对象属于语言内部的GeneratorType类型。从这方面看,所有生成器都是迭代器,因为GeneratorType类型的实例实现了迭代器接口。但是却可以编写不是生成器的迭代器,方法是实现经典的迭代器模式。

3.概念

在典型的迭代器设计模式中,迭代器用于遍历集合,从中产生元素。迭代器可能相当复杂,例如遍历树状数据结构。但是,不管典型的迭代器中有多烧逻辑,都是从现有的数据源中读取值;而且,调用next(it)时,迭代器不能修改从数据源中读取的 值,只能原封不动地产出值。

而生成器可能无需遍历集合就能生成值,例如range函数,即便依附集合,生成器不仅能产生集合中的元素,还可能产出派生自元素的其他值。从这方面讲,生成器不都是迭代器。

从概念方面来说,不使用生成器对象也能编写生成器

11.7 迭代器和生成器的常见应用

  • zip(): 同时迭代多个序列,zip(a, b)会生成一个可返回元组(x, y)的迭代器,其中x来自ay来自b
list1 = [1,2,3,4,5,6]
list2 = ["a","b","c","d","e","f"]
list3 = [6,5,4,3,2,1]
for i,j,k in zip(list1, list2, list3):
    print(i,j,k)
1 a 6
2 b 5
3 c 4
4 d 3
5 e 2
6 f 1

利用zip()函数,我们还可把一个keys列表和一个values列表生成一个dict(字典)

keys = ["小明", "小李", "小王"]
values = [98,99,100]
dict1 = dict(zip(keys,values))
print(dict1)
{'小明': 98, '小李': 99, '小王': 100}
  • reversed():反向迭代,本质是一个生成器函数。

例如,实现列表的反转,例如:[1,3,2,4,5] -> [5,4,2,3,1]

def list_reverse(list1):
    list2 = [i for i in reversed(list1)]
    return list2

list1 = [1,5,6,7,9,12,3,5]
list2 = list_reverse(list1)
print(list2)
[5, 3, 12, 9, 7, 6, 5, 1]
  • map():map(function, iterable, ...)会根据提供的函数对指定序列做映射。第一个参数function以参数序列中的每一个元素调用function函数,返回包含每次function函数返回值的新列表。本质是一个生成器函数。
def square(x) : # 计算平方数
    return x ** 2

list1 = [1,2,3,4,5]
list2 = [i for i in map(square, list1)]   # 计算列表各个元素的平方
print(list2)
[1, 4, 9, 16, 25]

未完待续…

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YirongChen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值