Python迭代器和生成器的奥秘

在Python编程语言中,有许多强大的功能等待去发掘。热爱深入研究Python的各种细节,并理解它在不同情况下的反应。在与Python共事的时间里,遇到了一些功能,它们的使用频率与其简化的复杂性并不成正比。喜欢将这些称为Python中的“隐藏宝石”。这些功能并不为许多人所知,但对于分析和数据科学专业人士来说,它们非常有用。

Python的迭代器和生成器正是这类功能之一。它们的潜力是巨大的!如果曾经在处理大量数据时遇到内存不足的问题,那么会爱上Python中的迭代器和生成器的概念。与其一次性将所有数据放入内存,不如分批处理,只处理当前所需的数据,这样会大大减轻计算机内存的负担。这就是迭代器和生成器的作用!

将涵盖以下内容:

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

什么是可迭代对象?

可迭代对象是能够一次返回其成员的对象。这通常是通过for循环完成的。像列表、元组、集合、字典、字符串等对象被称为可迭代对象。简而言之,任何可以循环遍历的对象都是可迭代的。可以使用for循环逐个返回可迭代对象的元素。

什么是Python迭代器?

迭代器是一个代表数据流的对象,即可迭代对象。它们实现了Python中所谓的迭代器协议。那么,迭代器协议是什么呢?迭代器协议允许使用两个方法:__iter__()__next__()来遍历可迭代对象中的项。所有可迭代对象和迭代器都有__iter__()方法,该方法返回一个迭代器。迭代器跟踪可迭代对象的当前状态。

但是,将可迭代对象和迭代器区分开来的是__next__()方法,这只对迭代器可用。这允许迭代器在被请求时返回可迭代对象中的下一个值。让通过创建一个简单的可迭代对象(一个列表)并使用__iter__()方法从中创建一个迭代器来看看这是如何工作的:

# 假设有一个列表 my_list = [1, 2, 3, 4, 5] # 创建一个迭代器 iterator = iter(my_list) # 使用next()方法获取下一个值 print(next(iterator)) # 输出: 1

正如所说,可迭代对象有__iter__()方法来创建迭代器,但它们没有__next__()方法,这就是它们与迭代器的区别。所以,让再次尝试并尝试从列表中检索值:

# 继续使用迭代器获取下一个值 print(next(iterator)) # 输出: 2

完美!但是,等等,不是说迭代器也有__iter__()方法吗?那是因为迭代器也是可迭代的,但反之则不然。它们是它们自己的迭代器。让通过循环遍历迭代器来展示这个概念:

# 循环遍历迭代器 while True: try: print(next(iterator)) except StopIteration: break

如果超出了调用next()方法的次数限制,会发生什么呢?

# 尝试访问迭代器末尾之后的下一个值 print(next(iterator)) # 抛出StopIteration异常

没错,会得到一个错误!如果在到达可迭代对象的末尾后尝试访问下一个值,将引发StopIteration异常,这简单地表示“不能再进一步了!”可以处理这个错误,使用异常处理。实际上,可以自己构建一个循环来遍历可迭代项:

# 手动构建循环 for item in my_list: print(item)

如果退后一步,会意识到这正是for循环在幕后的工作方式。在这里手动做的循环,for循环自动做了同样的事情。这就是为什么for循环在遍历可迭代对象时更受青睐,因为它们自动处理异常。

在Python中创建迭代器

现在知道了Python迭代器的工作原理,可以更深入地了解如何从头开始创建一个迭代器,以便更好地理解事物的运作方式。将创建一个简单的迭代器来打印所有的偶数:

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

让分解这段Python代码:

  • __init__()方法是一个类构造函数,当调用类时首先执行。它用于分配类在程序执行期间可能需要的任何初始值。在这里将num变量初始化为2。
  • iter()next()方法是使这个类成为迭代器的方法。
  • iter()方法返回迭代器对象并初始化迭代。由于类对象本身是一个迭代器,因此它返回自身。
  • next()方法返回迭代器中的当前值,并为下一次调用更改状态。通过2更新num变量的值,因为只打印偶数。

可以通过创建它的对象然后对对象调用next()方法来遍历Sequence类:

# 创建Sequence类的实例并遍历 even_gen = EvenNumbers() print(next(even_gen)) # 输出: 2

由于没有提到任何确定序列结束的条件,迭代器将不断返回下一个值。但可以通过停止条件轻松更新它:

# 更新迭代器以包含停止条件 class EvenNumbers: def __init__(self): self.num = 2 def __iter__(self): return self def __next__(self): if self.num > 10: raise StopIteration else: result = self.num self.num += 2 return result

在这里,没有使用next()方法来从迭代器返回值,而是使用了for循环,它像以前一样工作。

熟悉Python中的生成器

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

def fib(): a, b = 0, 1 while a < 10: yield a a, b = b, a + b

注意到这个生成器函数和普通函数的区别了吗?是的,yield关键字!普通函数使用return关键字返回值。但生成器函数使用yield关键字返回值。这就是生成器函数与普通函数的区别(除了这个区别,它们完全相同)。

yield关键字像普通的return关键字一样工作,但具有额外的功能——它记住了函数的状态。所以下次生成器函数被调用时,它不会从头开始,而是从上次调用停止的地方继续。

# 使用生成器 fib_gen = fib() print(next(fib_gen)) # 输出: 0

生成器是“生成器”类型,这是一种特殊的迭代器,所以它们也是懒惰的工。它们不会返回任何值,除非明确地被next()方法要求。

最初,当创建fib()生成器函数的对象时,它初始化了prevcurr变量。现在,当对象上调用next()方法时,生成器函数计算值并返回输出,同时记住函数的状态。所以,下次调用next()方法时,函数从上次停止的地方继续。

函数将在每次被next()方法请求时继续生成值,直到prev大于5,此时将引发StopIteration错误,如下所示:

# 生成器直到抛出StopIteration错误 while True: try: print(next(fib_gen)) except StopIteration: break

在Python中实现生成器表达式

不必每次都编写函数来执行生成器。可以使用生成器表达式,就像列表推导式一样。唯一的区别是,与列表推导式不同,生成器表达式被括号包围,如下所示:

# 生成器表达式 gen_expr = (x for x in range(10)) print(next(gen_expr)) # 输出: 0

但它们仍然是懒惰的,所以需要使用next()方法。然而,现在知道使用for循环是返回值的更好选项:

# 使用for循环遍历生成器表达式 for value in gen_expr: print(value)

生成器表达式在想编写简单代码时非常有用,因为它们易于阅读和理解。但随着代码变得更加复杂,它们的功能迅速下降。这时会发现自己回到了生成器函数,这在编写更复杂的函数方面提供了更大的灵活性。

大问题——为什么应该依赖迭代器呢?在文章一开始就提到了——使用迭代器是因为它们为节省了大量的内存。这是因为迭代器在生成时不计算它们的项,而是在被调用时才计算。

如果创建一个包含1000万项的列表和一个包含相同数量项的生成器,它们的大小差异将是惊人的:

# 比较列表和生成器的大小 import sys list_size = sys.getsizeof([1] * 10000000) gen_size = sys.getsizeof((x for x in range(10000000))) print(list_size, gen_size)

对于相同大小的列表和生成器,它们的大小差异是巨大的。这就是迭代器的美妙之处。不仅如此,还可以使用迭代器逐行读取文本文件,而不是一次性读取所有内容。这将再次节省大量内存,特别是如果文件非常大的话。

在这里,使用生成器逐行读取文件。为此,可以创建一个简单的生成器表达式来延迟打开文件,即逐行读取:

# 使用生成器逐行读取文件 def read_file(file_name): with open(file_name, 'r') as file: for line in file: yield line

这一切都很好,但对于数据科学家或分析师来说,一切都归结为在Pandas数据框中处理大型数据集。想想不得不处理大型数据集的时候,也许有一个包含数千行数据点的数据集,甚至更多。如果Pandas有什么东西可以处理这个问题,作为数据科学家的生活将变得更加轻松。

好吧,很幸运,因为Pandas的read_csv()有一个chunksize参数来处理这个问题。它允许以指定的大小分块加载数据,而不是将所有数据加载到内存中。当完成处理一块数据后,可以对数据框对象执行next()方法来加载下一块数据。就是这么简单!

# 使用chunksize参数读取数据 import pandas as pd chunk_size = 10 for chunk in pd.read_csv('black_friday.csv', chunksize=chunk_size): print(chunk)
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485