详解 Python 生成器与迭代器 及其区别

前言

        不论是初学python还是python进阶,这都是绕不开的知识点,生成器与迭代器的概念相较于其他基础概念显得晦涩难懂,知识点囊括很多方面,查阅越多的资料,头就越大,现在这加以理解归纳总结。

相关概念

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

迭代器模式可用来:

  • 访问一个聚合对象的内容而无需暴露它的内部表示
  • 支持对聚合对象的多种遍历
  • 为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)

这就与设计模式相关了,具体了解可参考:遍历聚合对象中的元素——迭代器模式(二)_刘伟技术博客-CSDN博客

2. 迭代器协议:对象必须提供next()方法,它要么返回迭代中的下一项,要么就引起一个StopIteration异常,以终止迭代。

__iter__ 方法实例化并返回一个迭代器。

3. 可迭代对象:使用iter内置函数可以获取迭代器的对象,如果对象实现了能返回迭代器的__iter__ 方法或是实现了序列语义的 __getitem__ 方法,那么对象就是可迭代的,在python中所有集合都是可以迭代的,例如我们所熟悉的 list、tuple、dict、字符串都是Iterable(可迭代对象)。

可迭代对象不是迭代器,不过可以通过 iter() 函数获得⼀个 Iterator 对象。可迭代对象与迭代器之间的关系为:python从可迭代对象中获取迭代器。

我们可以通过isinstance()方法来判断是否为可迭代对象:

# 从python3.3开始用 collections.abc 代替 collections
from collections.abc import Iterable
# 列表
print(isinstance([], Iterable))  # True
# 列表推导式
print(isinstance([x for x in range(10)], Iterable))  # True
# 字典
print(isinstance({}, Iterable))  # True
# python3还有字典推导式
print(isinstance({i: i % 2 == 0 for i in range(10)}, Iterable))  # True
# 字符串
print(isinstance('', Iterable))  # True
# 元组推导式,也称生成器表达式
print(isinstance((x for x in range(10)), Iterable))  # True
# 生成器表达式
test = (x for x in range(10))
print(type(test))  # 
# 能直接调用next()方法
print(next(test))  # 0

4. StopIteration 异常:用于标识迭代的完成,防止出现无限循环的情况,在 __next__() 方法中我们可以设置在完成指定循环次数后触发 StopIteration 异常来终止迭代。

class StopIter:
    def __iter__(self):
        self.a = 1
        return self

    def __next__(self):
        if self.a <= 10:
            x = self.a
            self.a += 1
            return x
        else:
            raise StopIteration  # 抛出 StopIteration 异常


my_test = StopIter()
# 创建迭代器对象
my_iter = iter(my_test)

for x in my_iter:
    print(x)

5. 斐波那契(Fibonacci)数列:是一个非常简单的递归数列,除第一个和第二个数外,任意一个数都可由前两个数相加得到。

6. 协同程序:一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

        pyhton中当协程执行到yield关键字时,会暂停在那一行,等到主线程调用send方法发送了数据,协程才会接到数据继续执行。但是,yield让协程暂停,和线程的阻塞是有本质区别的。协程的暂停完全由程序控制,线程的阻塞状态是由操作系统内核来进行切换。因此,协程的开销远远小于线程的开销

        简单来说是指可以运行的独立函数调用,函数可以暂停或者挂起,并在需要的时候从程序离开的地方继续或者重新开始。

7. __getitem__:python的魔法方法,可以让对象实现迭代功能,这样就可以使用for循环来迭代该对象了。这个方法返回与指定键相关联的值,对序列来说,键应该是0 ~n-1的整数,其中n为序列的长度,对映射来说,键可以是任何类型。

序列可迭代的原因:iter函数

        解释器需要迭代对象x时,会自动调用iter()。

        内置的iter函数有以下的作用:

        ① 检查对象是否实现了 __iter__ 方法,如果实现了就调用它,获取一个迭代器。 

        ② 如果没有实现 iter 方法,但是实现了 __getitem__ 方法,而且其参数是从零开始的索引,Python 会创建一个迭代器,尝试按顺序(从索引 0 开始)获取元素。 

        ③ 如果前面两步都失败,Python 抛出 TypeError 异常,通常会提示“C objectis not iterable”(C 对象不可迭代),其中 C 是目标对象所属的类。

        序列都可以迭代的原因是,它们都实现了 __getitem__ 方法,其实标准的序列也都实现了 __iter__ 方法。

        如果对象实现了能返回迭代器的 __iter__ 方法,那么对象就是可迭代的。

迭代器

        迭代器支持:

  • for 循环
  • 构建和扩展集合类型
  • 逐行遍历文本文件
  • 列表推导、字典推导和集合推导
  • 元组拆包
  • 调用函数时,使用 * 拆包实参

元组拆包可参考:Python---元组拆包(Tuple Unpacking)_Milkha的博客-CSDN博客_tuple拆分

        基础的for循环和enumerate函数就展示了python的迭代特性,迭代是Python最强大的功能之一,是访问集合元素的一种方式,它类似于循环,每一次重复的过程被称为一次迭代,每一次迭代的结果被用来作为下一次迭代的初始值,提供迭代方法的容器称为迭代器。        

创建迭代器对象

blog_user = "Yy_Rose"
# 创建迭代器对象
iteration = iter(blog_user)
print(next(iteration))  # Y
print(next(iteration))  # y
print(next(iteration))  # _
print(next(iteration))  # R
print(next(iteration))  # o
print(next(iteration))  # s
print(next(iteration))  # e
print(next(iteration))  # 报StopIteration异常,超出可迭代范围

        注:next(Iterator),只有迭代器对象才能调用next方法,可迭代对象直接调用会报错:'str' object is not an iterator

        上述方法会产生异常报错也很冗长,所以我们可以将程序改动一下,输出结果是一样的:

blog_user = "Yy_Rose"
iteration = iter(blog_user)
# 循环判断
while True:
    # 异常捕捉
    try:
        results = next(iteration)
        print(results)
    except StopIteration:
        # 释放iteration对象,即废弃迭代器对象
        del iteration
        # 跳出循环,程序终止
        break

        迭代器对象还可以使用常规for语句进行遍历:

blog_user = "Yy_Rose"
iteration = iter(blog_user)

for x in iteration:
    print(x, end=' ')  # Y y _ R o s e

        把一个类作为一个迭代器: 

class IterTest:
    def __iter__(self):
        self.a = 1
        return self

    def __next__(self):
        x = self.a
        self.a += 1
        return x


mytest = IterTest()
myiter = iter(mytest)

print(next(myiter))  # 1 
print(next(myiter))  # 2 
print(next(myiter))  # 3 
print(next(myiter))  # 4 
print(next(myiter))  # 5
print(next(myiter))  # StopIteration异常

        由上可知,迭代操作提供两个python内置函数:容器对象调用iter()方法得到它的迭代器;容器对象调用next()方法返回下一个值,如果没有返回值就会抛出StopIteration异常。 

        一个容器如果是迭代器,则必须实现 __iter__ 和 __next__ 魔法方法。

标准的迭代器接口有两个方法:

        __iter__ : 返回迭代器本身,实际上相当于 return self,以便在应该使用可迭代对象的地方使用迭代器。例如在for循环中。

        __next__ : 返回下一个可用元素,如果没有元素了,则抛出StopIteration异常,决定了迭代器的迭代规则。

        __iter__() 只会被调用一次,而 _next_() 会被调用 n 次,直到出现StopIteration异常。

        迭代器接口在collocations.abc.Iterator抽象基类中制定,这个类定义了 __next__ 抽象方法,而且继承子Iterable类;__iter__ 抽象方法则在Iterable类中定义,如图所示:

详解 Python 生成器与迭代器 及其区别_第1张图片

图解:iterable和iterator抽象基类,具体的Iterable.__iter__ 方法应该返回一个Iterator实例,具体的Iterator类必须实现 __next__ 方法,Iterator.__iter__ 方法直接返回实例本身。

下面以斐波那契数列举个例子:

class Fibonacci:
    def __init__(self, n):
        # 定义初值
        self.a = 0
        self.b = 1
        self.n = n

    def __iter__(self):
        # 返回迭代器本身
        return self

    def __next__(self):
        """
        不同于 a = b, b = a + b
        这里是先计算等式右边,再赋值给等式左边
        """
        self.a, self.b = self.b, self.a + self.b
        # 设置迭代范围
        if self.a > self.n:
            raise StopIteration  # 抛出StopIteration异常,迭代终止
        return self.a


# 传入迭代范围, n=10
fib_num = Fibonacci(10)
for num in fib_num:
    print(num)  # 1 1 2 3 5 8

        迭代器是一个可以记住遍历的位置的对象,迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束,迭代器只能往前不会后退

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

        所以迭代器是这样的对象:实现了无参数的 __next__ 方法,返回序列中的下一个元素;如果没有元素了,那么抛出StopIteration异常。Python中的迭代器还实现了 __iter__ 方法,因此迭代器也可以迭代。

深入分析iter函数

        如上可知,在Python中迭代对象x时会调用iter(x)。可是,iter函数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调用的对象创建迭代器。这样使用时,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值了第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出StopIteration异常,而不产出哨符。

        语法:iter(object, [sentinel])

        下面示例用iter函数掷骰子,知道掷出1点为止:

import random


def d6():
    return random.randint(1, 6)


d6_iter = iter(d6, 1)
print(d6_iter)  # 
for roll in d6_iter:
    print(roll, end=' ')

        注意:这里的iter函数返回一个 callable_iterator 对象。

        示例中的for循环输出的结果每次都不一样,可能长可能短,但是肯定不会打印1,因为1是哨符。与常规的迭代器一样,这个示例中的d6_iter对象一旦耗尽就没用了。如果想重新开始,必须再次调用iter(...),重新构建迭代器。

生成器

        为了抽象出迭代器模式,Python2.2(2001年)加入了yield关键字,来构建生成器(generator)。

        在调用生成器运行的过程中,每次遇到 yield 关键字时函数会暂停并保存当前所有的运行信息,返回 yield 语句的值, 并在下一次执行 next() 方法时从当前位置继续运行,带有 yield 语句的函数不再是一个普通函数,python 解释器会将其视为一个 生成器(generator),所以只要python函数的定义体中有yield关键字,该函数就是生成器函数。

        跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。

        对于一个普通的函数来说,调用它一般都是从函数的第一句开始找,有异常或执行了return语句亦或是函数的所有语句都执行完毕则为结束, 一旦函数将控制权交还给了调用者,就意味着工作结束了,因为函数所有的工作保存都是局部变量,局部变量调用结束则会消失,而生成器就是一个特殊的函数,它的调用可以中断或者暂停,可理解为一个断点,暂停后将控制权临时交出,在需要的时候再获取回来重新获得控制权,然后从上一次暂停的位置继续下去。     

        简单理解就是:延迟操作,是在需要的时候才产生结果,不是立即产生结果。 

生成器示例

def yield_test():
    print('这一行被执行')
    yield 1
    yield 2


run_test = yield_test()
print(next(run_test))  # 输出:这一行被执行   1
print(next(run_test))  # 输出:2
print(next(run_test))  # 输出:报StopIteration异常

        这个例子能很直观的看出,在直接调用next()方法进行下一次迭代时,生成器会从yield语句的下一句开始执行,直至遇到下一个yield语句。

        生成器函数会创建一个生成器对象,包装生成器函数的定义体。把生成器传给next(...)函数时,生成器函数会向前,执行函数定义体中的下一个yield语句,返回产出的值,并在函数定义体的当前位置暂停。最终,函数的定义体返回时,外层的生成器对象会抛出StopIteration异常,这与迭代器协议一致。

        用yield语句实现斐波那契数列:

def fibonacci(border):
    a, b = 0, 1
    while True:
        if b < border:
            yield b
            a, b = b, a + b
        else:
            break


for num in fibonacci(10):
    print(num, end=' ')  # 1 1 2 3 5 8

        判断一个函数是否是一个特殊的 generator 函数,以上示为例: 

from inspect import isgeneratorfunction

# 使用isgeneratorfunction()函数判断
print(isgeneratorfunction(fibonacci))  # Ture

生成器表达式

        生成器表达式可以理解为列表推导式的惰性版本:不会迫切的构建列表,而是返回一个生成器,按需惰性生成元素。也就是说,如果列表推导是制造列表的工厂,那么生成器表达式就是制造生成器的工厂。 

# 定义生成器函数
def generater_test():
    print('start')
    yield 'A'
    print('continue')
    yield 'B'
    print('end')


# 列表推导式
gen_list1 = [x*3 for x in generater_test()]
# start
# continue
# end

# for循环迭代gen_list1列表
for i in gen_list1:
    print('--->', i)
# ---> AAA
# ---> BBB

# 生成器表达式
gen_list2 = (x*3 for x in generater_test())
# gen_list2是一个生成器对象
print(gen_list2)  #  at 0x0000016F37F90EB0>

# for循环迭代gen_list2
for j in gen_list2:
    print('--->', j)
# start
# ---> AAA
# continue
# ---> BBB
# end

         for循环迭代gen_list2时,generater_test()函数体才会真正执行,for循环每次迭代时会隐式调用next(gen_list2),前进到generater_test()函数中的下一个yield语句,注意,generater_test()函数的输出与for循环中print函数的输出夹杂在一起。

        使用生成器表达式取代列表推导式可以同时节省 cpu 和 内存(RAM)空间,提高运行效率。

        现在,即使是内置的range()函数也返回一个类似生成器的对象,而以前则是返回完整的列表,如果一定要让range()函数返回列表,那么必须明确指明,例如:list(range(100))。

yield与return的区别

  • yield

        包含 yield 关键字的函数体,返回的是一个生成器对象,该对象可以迭代遍历和通过 next() 方法取出对象中的值。能节省内存空间,可以达到随用随取的效果。

  • return

        用于结束函数体的运行,return 后面的代码块不会执行,返回该函数的执行结果。

        在一个生成器函数中,如果没有 return,则默认执行至函数完毕,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代。

yield中的send()方法

        send()方法有一个参数,该参数指定的是上一次被挂起的yield语句的返回值。

        send()方法和next方法的区别在于:执行send()方法会首先把上一次挂起的yield语句的返回值通过参数设定,从而实现与生成器方法的交互。简单的说send()方法是传递yield表达式的值进去,而next()方法不能传递特定的值。

def gen_func():
    while True:
        x = yield
        print("value:", x)


# 调用生成器函数
gen = gen_func()
next(gen)  # 程序运行到yield就停住了, 等待下一个next
gen.send(1)  # 给yield发送值1, 然后这个值被赋值给了x, 并且打印出来, 然后继续下一次循环停在yield处
gen.send(2)
next(gen)  # 没有给x赋值, 执行print语句, 打印出None, 继续循环停在yield处

# value: 1
# value: 2
# value: None

        send()方法具有两种功能:第一,传值,send()方法,将其携带的值传递给yield,注意,是传递给yield,而不是x,然后再将其赋值给x;第二,send()方法具有和next()方法一样的功能,也就是说,传值完毕后,会接着上次执行的结果继续执行,直到遇到yield停止。

def gen_func():
    # 生成器:
    # 1. 可以产出值
    # 2. 可以接收值(send方法传递进来的值)
    gen_info = yield 1
    print(gen_info)
    yield 2
    yield 3


# 调用生成器函数
gen = gen_func()
# 在调用send发送非none值之前, 我们必须启动一次生成器
# 方式有两种: 1. gen.send(None), 2. next(gen)
restart = gen.send(None)
string = 'Yy_Rose'
# send方法可以传递值进入生成器内部, 同时还可以重启生成器执行到下一个yield位置
print(gen.send(string))  # Yy_Rose 2
print(gen.send(string))  # 3
# Yy_Rose
# 2
# 3

生成器与迭代器的区别

        生成器是迭代器的一种实现,所有生成器都是迭代器,因为生成器完全实现了迭代器接口,迭代器用于从集合中取出元素,而生成器用于“凭空”生成元素,生成器相较于迭代器更为简洁,它能极大程度的简化代码,使得执行流程更为清晰。

        迭代器需要我们定义一个类来实现相关的方法才能构造一个灵活的迭代器,而生成器则只需要在普通的函数中加入一个yield关键字,yield 语句的作用就是把一个函数变成一个生成器(generator),生成器的出现使得python能类似于协同程序(协同程序的概念一开始有介绍)工作。

总结

       以上是对于python生成器与迭代器的归纳总结,如有错误亦或是新的见解还望指出,谢谢~

       推荐观看底下的学习笔记,值得参考,能帮助理解:Python3 迭代器与生成器 | 菜鸟教程

你可能感兴趣的:(python,python)