5

连Python生成器(Generator)的原理都解释不了,还敢说Python用了5年?

 3 years ago
source link: https://blog.csdn.net/nokiaguy/article/details/109040591
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)的原理都解释不了,还敢说Python用了5年?

目录

1. 迭代

2. 生成器(Generator)

3. yield

4. 用普通函数模拟生成器函数的效果

5.与迭代相关的API


最近有很多学Python同学问我,Python Generator到底是什么东西,如何理解和使用。Ok,现在就用这篇文章对Python Generator做一个敲骨沥髓的深入解析。

为了更好地理解生成器(Generator),还需要掌握另外两个东西:yield和迭代(iterables)。下面就迭代、n成器和yield分别做一个深入的解析。

当创建一个列表对象后,可以一个接一个读取列表中的值,这个过程就叫做迭代。

mylist对象是可迭代的。在创建列表时,可以使用列表推导表达式,所以从直观上看,列表是可迭代的。

只要使用for ... in...语句,那么in子句后面的部分一定是一个可迭代的对象,如列表、字典、字符串等。

这些可迭代的对象在使用上非常容易理解,我们可以用自己期望的方式读取其中的值。但会带来一个严重的问题。就拿列表为例,如果需要迭代的值非常多,这就意味着需要先将所有的值都放到列表中,而且即使迭代完了列表中所有的值,这些值也不会从内存中消失(至少还会存在一会)。而且如果这些值只需要迭代一次就不再使用,那么这些值在内存中长期存在是没有必要的,所有就产生了生成器(Generator)的概念。

2. 生成器(Generator)

要理解生成器,首先要清楚生成器到底要解决什么问题,以及生成器的特性。

生成器只解决一个问题,就是让需要迭代的值不再常驻内存,也就是解决的内存资源消耗的问题。

为了解决这个问题,生成器也要付出一定的代价,这个代价就是产生器中的值只能访问一次,这也是产生器的特性。

下面先看一个最简单的生成器的例子:

乍一看这段代码,好像与前面的代码没什么区别。其实,只有一点点区别,就是在创建data_generator对象时使用了一对圆括号,而不是一对方括号。使用一对方括号创建的是列表对象,而使用一对圆括号创建的就是迭代器对象,如果直接输出,会输出迭代器对象的地址,只有通过for...in...语句或调用迭代器的相应方法才能输出迭代器对象中的值。而且第二次对迭代器对象进行迭代,什么都不会输出,这是因为迭代器只能被迭代一次,而且被迭代的值使用完,是不会再保存在内存中的。有点类似熊瞎子摘苞米,摘一穗,丢一穗。

执行这段代码,会输出如下结果:

3. yield

到现在为止,我们已经对生成器要解决的问题,以及特性有了一个基本了解,那么产生器是如何做到这一点的呢?这就要依靠yield语句了。

现在让我们先来看一个使用yield的例子。

这段代码的目的是输出不大于10的所有偶数,其中generator_even是一个生成器函数。我们注意到,在该函数中每找到一个偶数,就会通过yield语句指定这个偶数。那么这个yield起什么作用呢?

再看看后面的代码,首先调用generator_even函数,并将返回值赋给even_generator变量,这个变量的类型其实是一个生成器对象。而for...in...循环中的in子句后面则是这个生成器对象,而n则是生成器中的每一个值(偶数)。执行这段代码,会输出如下结果:

现在先谈谈执行yield语句会起到什么效果。其实yield语句与return语句一样,都起到返回的作用。但yield与return不同,如果执行return语句,会直接返回return后面表达式的值。但执行yield语句,返回的是一个生成器对象,而且这个生成器对象的当前值就是yield语句后面跟着的表达式的值。调用yield语句后,当前函数就会返回一个迭代器,而且函数会暂停执行,直到对该函数进行下一次迭代。

可能读到这些解释,有的读者还是不太明白,什么时候进行下一次迭代呢?如果不使用for...in...语句,是否可以对生成器进行迭代呢?其实迭代器有一个特殊方法__next__。每次对迭代器的迭代,本质上都是在调用__next__方法。

那么还有最后一个问题,for...in...语句在什么时候才会停止迭代呢?其实for...in...语句在底层会不断调用in子句后面的可迭代对象的__next__方法,直到该方法抛出StopIteration异常为止。也就是说,可以将上面的for...in...循环改成下面的代码。连续调用6次__next__方法,返回0到10,一共6个偶数,当第7次调用__next__方法时,生成器中已经没有值了,所以会抛出StopIteration异常。由于for...in...语句自动处理了StopIteration异常,所以循环就会自动停止,但当直接调用__next__方法时,如果生成器中没有值了,就会直接抛出StopIteration异常,除非使用try...except...语句捕获该异常,否则程序会异常中断。

总结:生成器本质上就是动态产生待迭代的值,使用完就直接扔掉了,这样非常节省内存空间,但这些值只能被迭代一次。

4. 用普通函数模拟生成器函数的效果

如果你看到一个函数中使用了yield语句,说明该函数是一个生成器。其实可以按下面的步骤将该生成器函数改造成普通函数。

1.  在函数的开始部分定义一个列表变量,代码如下:

result = []

2. 将所有的yield expr语句都替换成下面的语句:

result.append(expr)

3. 函数的最后执行return result返回这个列表对象

为了更清晰表明这个转换过程,现在给出一个实际的案例:

在这段代码中有两个函数:generate_even和generate_even1,其中generate_even是生成器函数,generate_even1是普通函数(与generate_even函数的功能完全相同)。按着前面的步骤,将所有产生的偶数都添加到了列表变量evens中,最后返回这个列表变量。这两个函数在使用方式上完全相同。不过从本质上说,generate_even函数是动态生成的偶数,用完了就扔,而generate_even1函数事先将所有产生的偶数都添加到列表中,最后返回。所以从generate_even1函数的改造过程来看,yield的作用就相当于使用append方法将表达式的值添加到列表中,只不过yield并不会保存表达式的值,而append方法会保存表达式的值。

5.与迭代相关的API

这一节来看一看Python为我们提供了哪些与迭代相关的API

Python SDK提供了一个itertools模块,该模块中的API都与迭代相关,例如,可以通过chain.from_iterable方法合并多个可迭代对象,通过permutations函数以可迭代对象形式返回列表的全排列。

执行这段代码,会输出如下内容:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK