专栏首页数据派THU独家 | 什么是Python的迭代器和生成器?(附代码)

独家 | 什么是Python的迭代器和生成器?(附代码)

作者:Aniruddha Bhandari 翻译:王琦

校对:和中华

本文约3700字,建议阅读10分钟。

本文介绍了Python中的生成器和迭代器。在处理大量数据时,计算机内存可能不足,我们可以通过生成器和迭代器来解决该问题。

迭代器:一次一个!

Python 是一种美丽的编程语言。我喜欢它提供的灵活性和难以置信的功能。我喜欢深入研究Python的各种细微差别,并了解它如何应对不同的情况。

在使用Python的过程中,我了解到了一些功能,这些功能的使用与其简化的复杂度不相称。我喜欢称它们为Python中“隐藏的宝石”。很多人对此并不了解,但对于分析和数据科学专家来说,它们非常有用。

Python迭代器和生成器正好属于这一类。它们的潜力是巨大的!

如果你曾经在处理大量数据时遇到麻烦(谁没有呢?!),并且计算机内存不足,那么你会喜欢Python中的迭代器和生成器的概念。

与其将所有数据一次性都放入内存中,不如将它按块处理,只处理当时所需的数据,对吗?这将大大减少我们计算机内存的负载。这就是迭代器和生成器的作用!

因此,让我们仔细读读本文,探索Python迭代器和生成器的世界吧。

我假设你熟悉Python的基础知识。如果没有,我建议你先从下面的热门课程学起:

Python数据科学:

https://courses.analyticsvidhya.com/courses/introduction-to-data-science?utm_source=blog&utm_medium=python-iterators-and-generators

这是我们要介绍的内容:

  • 什么是可迭代对象?
  • 什么是Python迭代器?
  • 在Python中创建一个迭代器
  • 熟悉Python中的生成器
  • 实现Python中的生成器表达式
  • 为什么你应该使用迭代器?

什么是可迭代对象?

可迭代对象是能够一次返回其一个成员的对象”。

通常使用for循环完成此操作。像列表、元组、集合、字典、字符串等等之类的对象被称为可迭代对象。简而言之,任何你可以循环的对象都是可迭代对象。

我们可以使用for循环逐个地返回可迭代的元素。在这里,我们使用for循环遍历列表的元素:

# iterables  
sample = ['data science', 'business analytics', 'machine learning']  
for i in sample:  
    print(i)

既然我们知道了什么是可迭代对象,那么实际上我们是如何遍历这些值的?以及我们的循环如何知道何时停止?进入到迭代器部分!

什么是Python迭代器?

迭代器是代表数据流的对象,即可迭代。它们在Python中实现了迭代器协议。这是什么?

好吧,迭代器协议允许我们在一个可迭代对象中使用两种方法来循环遍历项:__iter __()和__next __()。所有的可迭代对象和迭代器都有__iter __()方法,该方法返回一个迭代器。

迭代器跟踪可迭代对象的当前状态。

但可迭代对象和迭代器不同之处在于__next __()方法只能由迭代器访问。这使得无论何时只要我们要求迭代器返回下一个值,迭代器就会返回下一个值。

让我们创建一个简单的可迭代对象、本例中为一个列表以及使用__iter __()方法来构造一个迭代器来了解其工作原理:

sample = ['data science', 'business analytics', 'machine learning']  
# generating an iterator  
it = sample.__iter__()  
print(it)  
# iterables do not have __next__() method  
sample.__next__()  

是的,正如我所说,可迭代对象有用于创建迭代器的__iter __()方法,但它们没有仅迭代器才有的__next __()方法。因此,让我们再试一次,然后尝试从列表中检索值:

sample = ['data science', 'business analytics', 'machine learning']  
# generating an iterator  
it = sample.__iter__()  
print(it.__next__())  
print(it.__next__())  
print(it.__next__()) 

完美!但等一下,我不是说迭代器也具有__iter __()方法吗?那是因为迭代器也是可迭代的,但反过来不成立。它们是自己的迭代器。让我通过遍历迭代器向你展示这个概念:

sample = ['data science', 'business analytics', 'machine learning']  
it = sample.__iter__()  
itit = it.__iter__()  
print(type(itit))  
print(itit.__next__())  
print(itit.__next__())  
print(itit.__next__())  

酷!但我们可以使用iter()和next()来代替__iter__()和__next__()方法,它们提供了一种更简洁的方法:

sample = ['statistics', 'linear algebra', 'probability']  
  
# iterator  
it = iter(sample)  
  
# next values  
print(next(it))  
print(next(it))  
print(next(it))

但如果我们超过了调用next()方法的限制次数,该怎么办?这会发生什么呢?

print(next(it))  

是的,我们得到了一个错误!如果我们在到达迭代器的末尾之后尝试访问下一个值,则会引起StopIteration异常,该异常的意思是“你不能更进一步了!”。

我们可以使用异常处理来处理此错误。实际上,我们可以自己构建一个循环来遍历可迭代的项:

sample = ['statistics', 'linear algebra', 'probability']  
it = iter(sample)  
while True:  
    # this will execute till an error is raised  
    try:  
        val = next(it)  
    # when we reach end of the list, error is raised and we break out of the loop  
    except StopIteration:  
        break  
    print(val) 

如果你退后一步,你会意识到,这正是for循环在底层运行的方式。我们在此处手动循环中所做的操作,for循环会自动执行相同的操作。这就是为什么for循环比遍历可迭代对象更可取,因为它们会自动处理异常。

每当我们迭代一个可迭代对象时,for循环通过iter()知道要迭代的项,并使用next()方法返回后续的项。

在Python中创建一个迭代器

既然我们知道了Python迭代器是如何工作的,我们可以更深入地研究并从头开始创建一个迭代器,以更好地了解其是如何凑效的。

我将创建一个用于打印所有偶数的简单迭代器:

class Sequence():  
    def __init__(self):  
        self.num = 2  
    def __iter__(self):  
        return self  
    def __next__(self):  
        val = self.num  
        self.num += 2  
        return val 

让我们分解一下这段Python代码:

  • __init __()方法是类构造函数,调用类时会首先执行该函数。它用于分配程序执行期间类最初所需的任何值。我在这里设置num变量的初始值为2;
  • iter()和next()方法使这个类变成了迭代器;
  • iter()方法返回迭代器对象并对迭代进行初始化。由于类对象本身是迭代器,因此它返回自身;
  • next()方法从迭代器中返回当前值,并改变下一次调用的状态。我们将num变量的值加2,因为我们只打印偶数。

我们可以创建Sequence对象来遍历Sequence类,在该对象上调用next()方法:

it = Sequence()  
print(next(it))  
print(next(it))  
print(next(it))  
print(next(it))  
print(next(it)) 

我没有写sequence结束的条件,因此迭代器将永远继续返回下一个值。但我们可以使用停止条件轻松地对其进行更新:

class Sequence():  
    def __init__(self):  
        self.num = 2  
    def __iter__(self):  
        return self  
    def __next__(self):  
        val = self.num  
        if val>=10:  
            raise StopIteration  
        self.num += 2  
        return val    

我刚刚加入了一条if语句,只要值超过10,该语句就会停止迭代:

it = Sequence()  
for i in it:  
     print(i)

在这里,我没有使用next()方法从迭代器返回值,而是使用了for循环,该循环的工作方式与之前相同。

熟悉Python中的生成器

生成器也是迭代器,但更加优雅。使用生成器,我们可以实现与迭代器相同的功能,但不必在类中编写iter()和next()函数。相反,我们可以使用一个简单的函数来完成与迭代器相同的任务:

# fibonacci sequence using a generator   
def fib():     
    prev, curr = 0, 1  
    # infinite loop  
    while prev<5:  
        value = prev  
        # Calculate the next number in the sequence. Using Tuple unpacking.  
        prev, curr = curr, prev + curr  
        # yield the value  
        yield value  

你是否注意到这个生成器函数和常规函数的不同?是的,yield关键字!

普通函数使用return关键字返回值。但是生成器函数使用yield关键字返回值。这就是生成器函数与常规函数不同的地方(除了这种区别,它们是完全相同的)。

yield关键字的工作方式类似于普通的return关键字,但有额外的功能:它能记住函数的状态。因此,下次调用generator函数时,它不是从头开始,而是从上次调用中停止的位置开始。

让我们看看它是如何工作的:

# generator object  
gen=fib()  
print(gen)  
# values  
print(next(gen))  
print(next(gen))  
print(next(gen))  
print(next(gen))  
print(next(gen))  

生成器属于“生成器”类型,它是迭代器的一种特殊类型,但仍然是迭代器,因此它们也是懒惰的工作者。除非next()方法明确要求它们这样做,否则它们不会返回任何值。

最初创建fib()生成器函数的对象时,它会初始化prev和curr变量。现在,当在对象上调用next()方法时,生成器函数会计算值并返回输出,同时记住函数的状态。因此,下次调用next()方法时,该函数将从上次停止的地方开始,从那里继续。

每当使用next()方法时,该函数将继续生成值,直到prev变得大于5,这时将引起StopIteration异常,如下所示:

print(next(gen))

实现Python中的生成器表达式

你不必在每次执行生成器时都编写函数。相反,你可以使用生成器表达式,就像列表生成式一样。唯一的区别是,与列表生成式不同,生成器表达式包含在圆括号内,如下所示:

squared_gen = (x*x for x in range(2,5))  
print(squared_gen)

但它们仍然很懒,因此你需要使用next()方法。但你现在知道使用for循环可以更好地返回值:

for i in squared_gen:  
    print(i) 

当你编写简单的代码时,生成器表达式非常有用,因为它们易读、易理解。但随着代码变得更复杂,它们的功能会迅速变弱。在这种情况下,你发现自己会重新使用生成器函数,生成器函数在编写更复杂的函数方面提供了更大的灵活性。

为什么你应该使用迭代器?

一个重要的问题:为什么要先考虑用迭代器?

我在文章开头提到了这一点:之所以使用迭代器,是因为它们为我们节省了大量内存。这是因为迭代器在生成时不会计算项,而只会在调用它们时计算。

如果我创建一个包含1000万个项的列表,并创建一个包含相同数量项的生成器,则它们内存大小上的差异将令人震惊:

import sys  
# list comprehension  
mylist = [i for i in range(10000000)]  
print('Size of list in memory',sys.getsizeof(mylist))  
# generator expression  
mygen = (i for i in range(10000000))  
print('Size of generator in memory',sys.getsizeof(mygen)

对于相同的数量的项,列表和生成器在内存大小上存在巨大差异。这就是迭代器的美。

不仅如此,你可以使用迭代器逐行读取文件中的文本,而不是一次性读取所有内容。这会再次为你节省大量内存,尤其是在文件很大的情况下。

在这里,让我们使用生成器来迭代读取文件。为此,我们可以创建一个简单的生成器表达式来懒惰地打开文件,一次读取一行:

file = "Greetings.txt"  
# generator expression  
lines = (line for line in open(file))  
print(lines)  
# print lines  
print(next(lines))  
print(next(lines))  
print(next(lines)) 

这很棒,但对于数据科学家或分析师而言,他们最终都要在Pandas的 dataframe中处理大型数据集。当你不得不处理庞大的数据集时,也许这个数据集有几千行数据点甚至更多。如果Pandas可以解决这一难题,那么数据科学家的生活将变得更加轻松。

好吧,你很幸运,因为Pandas的read_csv()(https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html)有处理该问题的chunksize参数。它使你可以按指定大小的块来加载数据,而不是将整个数据加载到内存中。处理完一个数据块后,可以对dataframe对象执行next()方法来加载下一个数据块。就这么简单!

我将读取Black Friday数据集(https://datahack.analyticsvidhya.com/contest/black-friday/?utm_source=blog&utm_medium=python-iterators-and-generators),该数据集包含550,068行数据,读取时设置每块的大小为10,这样做只是为了演示该函数的用法:

import pandas as pd  
  
# pandas dataframe  
df = pd.read_csv('./Black Friday.csv', chunksize=10)  
  
# print first chunk of data  
next(df)
# print second chunk of data  
next(df)  

很有用,不是吗?

结语

我确信你现在已经习惯于使用迭代器,而且一定在考虑把所有函数转换为生成器!你开始喜欢Python编程的强大之处。

你以前使用过Python迭代器和生成器吗?或者你要与社区分享其他“隐藏的宝石”?大家可以在下方评论!

原文标题:

What are Python Iterators and Generators? Programming Concepts Every Data Science Professional Should Know

原文链接:

https://www.analyticsvidhya.com/blog/2020/05/python-iterators-and-generators/

编辑:黄继彦

校对:谭佳瑶

本文分享自微信公众号 - 数据派THU(DatapiTHU),作者:数据派

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 收藏 | 27个机器学习小抄(附学习资源)

    数据派THU
  • 机器翻译:生于冷战,却为人类重建巴别塔

    来源:环球科学ScientificAmerican 作者:陈宗周 本文长度为5200字,建议阅读5分钟 本文回顾机器翻译发展史,并分析这个曾一度陷入低潮的领域,...

    数据派THU
  • 一文解读Tensor到底是个啥玩意儿?(附代码)

    本文介绍了各种数值型数据的容器(标量、向量、矩阵、张量)之间的关系,在实践中,张量特指3维及更高维度的数据容器。

    数据派THU
  • Python生成器

    通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅...

    用户1332428
  • 快速学习-Python迭代器和生成器

    迭代是Python最强大的功能之一,是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完...

    cwl_java
  • Python-生成器1.什么是生成器2.创建生成器方法 3.send 4.实现多任务 5.迭代器 6.闭包

    通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅...

    意气相许的许
  • 生成器&迭代器

    一.生成器 在介绍生成器表达式之前,先看下列表表达式: 1 >>> l = [i for i in range(50) if i % 2] #生成...

    用户1679793
  • 一文解开可迭代对象和迭代器的神秘面纱

    可迭代对象和迭代器是两种不同的数据类型,它们都在我们的编程中时常可以遇到。当然他们之间也有很大的关联,接下来就让我们把它们搞定。

    sergiojune
  • Python迭代器使用详解

    这一集的内容看起来比较绕,反反复复出现的是迭代二字。大家注意,这一节的内容很pythonic,是很有特色也非常重要的知识点。敲黑板啦!

    Python中文社区
  • 迭代器&生成器

    迭代器是访问集合元素的一种方式。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退,不过这也没什么,因为人们很少在迭代途中...

    hankleo

扫码关注云+社区

领取腾讯云代金券