• Python中的生成器


    生成器:generator

    Python生成器简介

    在本文中,我们将学习如何使用 Python 生成器轻松地创建迭代器、生成器与迭代器和普通函数有何不同,以及为什么应该使用生成器。

    在 Python 中构建迭代器(iterator)需要做大量的工作。我们必须使用 __iter__()__next__()方法实现一个类,跟踪内部状态,并在没有返回值时引发 StopIteration异常。

    这既冗长又违反直觉,在这种情况下,生成器(generator)可以拯救世界了。Python 生成器是创建迭代器的一种简单方法。上面提到的所有工作都是由生成器自动处理的,也就是说自动实现了__iter__()__next__()方法。

    简单来说,生成器是一个返回对象(迭代器)的函数,我们可以对其进行迭代(每次迭代一个值)。

    Python生成器的创建

    用 Python 创建生成器相当简单。这就像定义一个普通函数一样简单,但是使用 yield 语句而不是 return 语句。

    如果一个函数包含至少一个 yield 语句(它可能包含其他 yield 语句或 return 语句) ,它就成为一个生成器函数。yield 和 return 都会从函数返回一些值。

    不同之处在于,当 return 语句完全终止一个函数时,yield 语句暂停保存其所有状态的函数,并在以后的连续调用中继续执行。

    生成器 VS 函数

    下面是生成器函数与普通函数的区别。

    • 生成器函数包含一个或多个 yield 语句。
    • 生成器函数在调用时,返回一个对象(迭代器) ,但不会立即开始执行。
    • 生成器函数自动实现迭代器的两个魔法方法,因此可以使用next()迭代这些项。
    • 生成器函数一旦创建,函数将暂停并将控制转移到调用方。
    • 生成器函数的局部变量及其状态在连续调用之间被记住。
    • 当生成器函数终止时,在进一步调用时自动引发 StopIteration。

    这里有一个例子来说明上述所有要点。我们有一个名为 my_gen() 的生成器函数,它包含几个 yield 语句。

    # 一个简单的生成器函数
    def my_gen():
        n = 1
        print('第一次打印')
        # 生成器函数包含yield语句
        yield n
    
        n += 1
        print('第二次打印')
        yield n
    
        n += 1
        print('最后打印')
        yield n
    

    运行结果:

    # 返回一个生成器,但不会立即执行
    a = my_gen()
    
    # 使用next()函数进行迭代操作
    next(a)
    # 输出:第一次打印
    # 输出:1
    
    # 一旦生成器函数yield,函数会暂停并将控制转移到调用方
    
    # 局部变量及其状态在连续调用之间被记住
    
    next(a)
    # 输出:第二次打印
    # 输出:2
    
    next(a)
    # 输出:最后打印
    # 输出:3
    
    # 最终,函数终止。后续如果继续调用则会引发StopIteration异常。
    next(a)  # 将引起 StopIteration 异常
    

    在上面的例子中需要注意的一件有趣的事情是,变量 n 的值在每次调用之间都会被记住。

    与普通函数不同,当函数 yield 时,局部变量不会被破坏。此外,生成器对象只能迭代一次。要重新启动进程,我们需要使用类似 a = my_gen()这样的代码创建另一个生成器对象。

    最后要注意的是,我们可以直接使用 for 循环的生成器。

    这是因为 for 循环接受一个迭代器,并使用 next() 函数对其进行迭代。当 StopIteration 被引发时,它自动结束。

    如需了解在 Python 中 for 循环实际是如何实现的,请参阅“python迭代器”一文。

    示例:

    # 一个简单的生成器函数
    def my_gen():
        n = 1
        print('第一次打印')
        # 生成器函数包含yield语句
        yield n
    
        n += 1
        print('第二次打印')
        yield n
    
        n += 1
        print('最后打印')
        yield n
    
    
    # 使用for循环遍历
    for item in my_gen():
        print(item)
    

    当你运行这个程序时,输出结果会是:

    第一次打印
    1
    第二次打印
    2
    最后打印
    3
    

    带循环的Python生成器

    上面的例子用处不大,我们研究它只是为了搞清楚背后发生了什么。通常,生成器函数是通过具有适当终止条件的回路来实现的。

    让我们再来看一个生成器的例子,它用来反转字符串。

    # 生成器函数
    def rev_str(my_str):
        length = len(my_str)
        for i in range(length - 1, -1, -1):
            yield my_str[i]
    
    
    # 用来反转字符串的for循环
    for char in rev_str("hello"):
        print(char)
    

    执行后输出:

    o
    l
    l
    e
    h
    

    在这个例子中,我们使用 range()函数给 for 循环提供逆序的索引值。

    注意: 这个生成器函数不仅可以处理字符串,还可以处理列表、元组等其他类型的可迭代对象。

    Python 生成器表达式

    使用生成器表达式,可以很容易地动态创建简单的生成器,这使得构建生成器变得容易。

    与创建匿名函数的 lambda 函数类似,生成器表达式创建匿名生成器函数。

    生成器表达式的语法是:(item for item in iterable if condition)

    类似于 Python 中的列表推导式,不同的是列表推导式使用方括号[ ],而生成器使用圆括号( )代替。列表推导式和生成器表达式的主要区别在于,列表推导式生成整个列表,而生成器表达式生成一个项。

    并且,生成器是延迟执行(只有在被要求的时候才生产项)。由于这个原因,生成器表达式比等效的列表推导式具有更高的内存效率。

    # 初始化列表
    my_list = [1, 3, 6, 10]
    
    # 使用列表推导式将每一项进行平方
    list_ = [x**2 for x in my_list]
    
    # 使用生成器可以实现同样的效果
    generator = (x**2 for x in my_list)
    print(list_)
    print(generator)
    

    输出:

    [1, 9, 36, 100]
    <generator object <genexpr> at 0x7f5d4eb4bf50>
    

    我们可以在上面看到,生成器表达式没有立即生成所需的结果。相反,它返回一个生成器对象,该对象仅按需生成项。

    以下是我们如何开始从生成器获取元素项的方法:

    # 初始化列表
    my_list = [1, 3, 6, 10]
    
    a = (x**2 for x in my_list)
    print(next(a))
    print(next(a))
    print(next(a))
    print(next(a))
    next(a)
    

    当我们运行上面的程序时,我们得到以下输出:

    1
    9
    36
    100
    Traceback (most recent call last):
      File "<string>", line 15, in <module>
    StopIteration
    

    生成器表达式可以作为函数参数使用。当以这种方式使用时,圆括号可以被删除。

    sum(x**2 for x in my_list)  # 输出:146
    max(x**2 for x in my_list)  # 输出:100
    

    使用 Python 生成器

    有几个原因使生成器成为一个强大的实现。

    1. 易于实现

    与迭代器类相比,生成器可以以简洁明了的方式实现。下面是一个使用迭代器类实现2次幂序列的示例。

    这是迭代器的代码量:

    class PowTwo:
        def __init__(self, max=0):
            self.n = 0
            self.max = max
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.n > self.max:
                raise StopIteration
    
            result = 2 ** self.n
            self.n += 1
            return result
    

    上面的程序很长而且令人抓狂。现在,让我们用一个生成器函数来做同样的事情。

    这是生成器的代码(没有对比就没有伤害)

    def PowTwoGen(max=0):
        n = 0
        while n < max:
            yield 2 ** n
            n += 1
    

    由于生成器(generator)自动跟踪细节,实现起来简洁明了。

    1. 内存效率

    返回序列的普通函数将在返回结果之前,在内存中创建整个序列。如果在序列中的项目数量非常大,对内存的消耗非常大。

    相反,这种序列的生成器实现则对内存友好,因为它一次只生成一个项目,所以是首选的。

    1. 表示无限流

    生成器是表示无限数据流的优秀媒介。无限流不能存储在内存中,而且由于生成器一次只生成一个项,因此它们可以表示无限的数据流。

    下面的生成器函数可以生成所有的偶数(至少在理论上)。

    def all_even():
        n = 0
        while True:
            yield n
            n += 2
    
    1. 管道生成器

    多个生成器可用于管道一系列的操作。这是最好的说明使用一个例子。

    假设我们有一个生成器,它生成 Fibonacci 斐波拉契数列中的数字。我们还有另一个平方数的生成器。

    如果我们要求出斐波拉契数列中数字的平方和,我们可以通过以下方法,将生成器函数的输出结合在一起来实现。

    # 斐波拉契数列生成器
    def fibonacci_numbers(nums):
        x, y = 0, 1
        for _ in range(nums):
            x, y = y, x+y
            yield x
    
    # 平方和生成器
    def square(nums):
        for num in nums:
            yield num**2
    
    print(sum(square(fibonacci_numbers(10))))
    

    执行后输出:

    4895
    

    这种流水线操作非常有效,并且易于阅读。

    是的,简直酷到没朋友!

    ---END

  • 相关阅读:
    eclipse 配置SVN代理服务器
    jenkins 配置SVN 代理服务器
    记录服务器启动redis过程
    java牛客刷题2020年9月4日
    java牛客网错题2020年9月3日
    bootstrap-select 实现搜索,如果内容搜索不到显示到框内
    pandas教程5-合并 concat
    pandas教程-4导入导出
    pandas简单教程1
    AttributeError: module 'pandas' has no attribute 'Series'
  • 原文地址:https://www.cnblogs.com/amoyshmily/p/16047837.html
Copyright © 2020-2023  润新知