4

Python编程:如何搞定生成器(Generator)及表达式?来盘它!

 1 year ago
source link: https://www.51cto.com/article/721427.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Python编程:如何搞定生成器(Generator)及表达式?来盘它!

作者:传新视界 2022-10-27 13:58:32
主要介绍了生成器相关知识,用于更好的自定义迭代器。内容包括何为生成器?如何自定义生成器以及和普通函数的关键区别?如何实现生成器表达式?

在前面的篇章中,我们学习了迭代器,这是一个很好的工具,特别是当你需要处理大型数据集时。然而,在Python中构建自己的迭代器有点麻烦和耗时。你必须定义一个实现迭代器协议(__iter__()和__next__()方法)的新类。在这个类中,需要自己管理变量的内部状态并更新它们。此外,当__next__()方法中没有要返回的值时,需要抛出StopIteration异常。

有没有更好的实现方式呢?答案是肯定的!这就是Python的生成器(Generator)解决方案。下面就来盘盘它。

e791d4034eb0ee98d10519a8f8ab2a21e3a382.png

何为生成器?

为了更高效的构建自己的迭代器,在Python中对此有一个优雅的解决方案,这是很值得高兴的。Python所提供的生成器(Generator)是用来帮助我们轻松创建迭代器。Generator允许你声明一个行为类似迭代器的函数,也就是说,它可以在for循环中使用。简单言之,生成器(Generator)就是个返回迭代器对象的函数。因此,这也是创建迭代器的简单方法。在创建迭代器时,你不需要考虑所需的所有工作(如迭代协议和内部状等),因为Generator将处理所有这些工作。

接下来,我们更进一步,轻松学懂Python中生成器是如何工作的以及如何定义它们。

定义生成器

如前一节所述,生成器是Python中一种特殊类型的函数。此函数不返回单个值,而是返回一个迭代器对象。在生成器函数中,返回值使用yield语句而不是return语句。下面定义一个简单的生成器函数,代码清单如下:

a852e6396ef98998b79200d2edd78a6f91c87b.png

代码清单片段-01

在上述清单中,我们定义一个生成器函数。该函数执行yield语句而不是return关键字。yield语句使这个函数成为生成器。当我们调用这个函数时,它将返回(产生)一个迭代器对象。我们再来看看生成器的调用:

b90085037fbfd723bcc00545549393e70838b8.png

代码清单片段-02

调用生成器,通常就跟创建对象类似,调用生成器函数,并赋给变量。

运行程序输出结果如下:

Yielding First Item
A
Yielding Second Item
B
Yielding Last Item
C

在应用生成器代码中,我们调用firstGenerator()函数,它是一个生成器,并返回一个迭代器对象。我们将这个迭代器命名为myIter。然后在这个迭代器对象上调用next()函数。在每次next()调用中,迭代器按各自的顺序执行yield语句并返回一个项。

根据规则,此生成器函数不应该包含return关键字。因为如果它包含,那么return语句将终止此函数,也就无从满足迭代器的要求了。

现在,让我们通过for循环的帮助来定义一个更具有实际意义的生成器。在本例中,我们将定义一个生成器,它将连续跟踪生成从0开始的数字序列,直到给定的最大限制。

代码清单如下:

98034a515bd13c849140668002ea4585766d77.png

代码清单片段-03

运行程序输出结果类似如下:

0
1
2
3

在上述清单中,我们定义一个生成器函数,它生成从0到给定数字的整数。正如所见,yield语句在for循环中。请注意,n的值自动存储在连续的next()调用中。

有一点需要注意,在定义生成器时,返回值必须是yield语句,并不是说生成器不能出现return语句。只是通常把返回非None值return语句放在生成器最后,为StopIteration 异常添加附加信息,以便调用者处理。示例如下:

058fdc336a0b154158d1385c87b9603f94f500.png

代码清单片段-04

下面是未进行异常处理时运行程序输出结果类似如下:

99

100

Traceback (most recent call last):

File "……", line 11, in <module>

print(next(g))

StopIteration: 不支持大于100的数字生成!

若对程序进行了异常捕捉处理(try-except),显示结果更简明,自己运行试试看。

生成器与普通函数

如果一个函数至少包含一个yield语句,那么它就是生成器函数。如果需要,还可以包含其他yield或return语句。yield和return关键字都将从函数中返回一些东西。

return和yield关键字之间的差异对于生成器来说非常重要。return语句会完全终止函数,而yield语句会暂停函数,保存它的所有状态,然后在后续的调用中继续执行。

我们调用生成器函数的方式和调用普通函数一样。但在执行过程中,生成器在遇到yield关键字时暂停。它将迭代器流的当前值发送到调用环境,并等待下一次调用。同时,它在内部保存局部变量及其状态。

以下是生成器函数与普通函数不同的关键点:

  • ü Generator函数返回(生成)一个迭代器对象。你无需担心显式地创建此迭代器对象,yield关键字为你做了这个工作。
  • ü Generator函数必须包含至少一个yield语句。如果需要,它可能包括多个yield关键字。
  • ü Generator函数内部实现迭代器协议(iter()和next()方法)。
  • ü Generator函数自动保存局部变量及其状态。
  • ü Generator函数在yield关键字处暂停执行,并将控制权传递给调用者。
  • ü Generator函数在迭代器流没有返回值时自动引发StopIteration异常。

我们用一个简单的例子来演示普通函数和生成器函数之间的区别。在这个例子中,我们要计算前n个正整数的和。为此,我们将定义一个函数,该函数给出前n个正数的列表。我们将以两种方式实现这个函数,一个普通函数和一个生成器函数。

普通函数代码如下:

759576a42e667785a84353f23d68bc06fd9c36.png

代码清单片段-05

运行程序输出结果类似如下:

49999995000000

Elapsed Time in seconds: 1.2067763805389404

在代码清单中,我们定义一个普通函数,它返回前n个正整数的列表。当我们调用这个函数时,它需要一段时间来完成执行,因为它创建的列表非常庞大。它还使用了大量内存来完成此任务。

现在让我们为相同的操作定义一个生成器函数来实现,代码清单如下:

72b19cd764fc4becdc3456175605b5ee781df9.png

代码清单片段-06

运行程序结果类似如下:

49999995000000
(生成器模式)Elapsed Time in seconds: 1.0013225078582764

正如在生成器清单中所见,生成器在更短的时间内完成相同的任务,并且使用更少的内存资源。因为生成器是一个一个地生成项,而不是返回完整的列表。

性能改进的主要原因(当我们使用生成器时)是值的惰性生成。这种按需值生成的方式,会降低内存使用量。生成器的另一个优点是,你不需要等到所有元素都生成后才开始使用它们。

生成器表达式

有时候,我们需要简单的生成器来执行代码中相对简单的任务。这正是生成器表达式(Generator Expression)用武之地。可以使用生成器表达式轻松地动态创建简单的生成器。

生成器表达式类似于Python中的lambda函数。但要记住,lambda是匿名函数,它允许我们动态地创建单行函数。就像lambda函数一样,生成器表达式创建的是匿名生成器函数。

生成器表达式的语法看起来像一个列表推导式。不同之处在于,我们在生成器表达式中使用圆括号而不是方括号。请看示例:

7300350603db0088b1c826bfe79a7e1c3c34f5.png

运行结果类似如下:

49999995000000

(生成器模式)Elapsed Time in seconds: 1.0013225078582764

在上述清单中,我们在生成器表达式的帮助下定义了一个简单的生成器。下面是语法:cubes_gen = (i**3 for i in nums)。你可以在输出中看到生成器对象。正如所已经知的,为了能够在生成器中获取项,我们要么显式调用next()方法,要么使用for循环遍历生成器。接下来就打印cubes_gen对象中的项:

c1676f256ab5fe53efb089eefad7c355a629f3.png

运行程序,遍历出的元素项结果是否和列表推导式一样。

我们再看一个例子。来定义一个生成器,将字符串中的字母转换为大写字母。然后调用next()方法打印前两个字母。代码示例如下:

9881237380937d6a0a59292a4d924c14f12f76.png

运行输出结果如下:

M
A

生成器好处

生成器是非常棒的工具,特别是当需要在相对有限的内存中处理大型数据时。以下是在Python中使用生成器的一些主要好处:

1)内存效率:

假设有一个返回结果非常大序列的普通函数。例如,一个包含数百万项的列表。你必须等待这个函数完成所有的执行,并将整个列表返回给你。就时间和内存资源而言,这显然是低效的。另一方面,如果你使用生成器函数,它将一个一个地返回项,你将有机会继续执行下一行代码。而不需要等待函数执行列表中的所有项。因为生成器一次只给你一项。

2)延迟计算:

生成器提供了延迟(惰性)计算求值的功能。延迟计算是在真正需要值时计算值,而不是在实例化时计算值。假设你有一个大数据集要计算,延迟计算允许你在整个数据集仍在计算生成中可立即开始使用数据。因为如果使用生成器,则不需要整个数据集。

3)易实现和可读性:

生成器非常容易实现,并且提供了好的代码可读性。记住,如果你使用生成器,你不需要担心__iter__()和__next__()方法。你所需要的只是函数中一个简单的yield语句。

4)处理无限流:

当你需要表示无限的数据流时,生成器是非常棒的工具。例如,一个无限计数器。理论上,你不能在内存中存储无限流的,因为你无法确定存储无限流需要多少的内存大小。这是生成器真正发挥作用的地方,因为它一次只产生一项,它可以表示无限的数据流。它不需要将所有的数据流存储在内存中。

主要介绍了生成器相关知识,用于更好的自定义迭代器。内容包括何为生成器?如何自定义生成器以及和普通函数的关键区别?如何实现生成器表达式?并总结了生成器的有点。通过这篇文章,相信你能更轻松高效的掌握Python常规的生成器方方面面。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK