在Python编程语言中,有许多强大的功能等待去发掘。热爱深入研究Python的各种细节,并理解它在不同情况下的反应。在与Python共事的时间里,遇到了一些功能,它们的使用频率与其简化的复杂性并不成正比。喜欢将这些称为Python中的“隐藏宝石”。这些功能并不为许多人所知,但对于分析和数据科学专业人士来说,它们非常有用。
Python的迭代器和生成器正是这类功能之一。它们的潜力是巨大的!如果曾经在处理大量数据时遇到内存不足的问题,那么会爱上Python中的迭代器和生成器的概念。与其一次性将所有数据放入内存,不如分批处理,只处理当前所需的数据,这样会大大减轻计算机内存的负担。这就是迭代器和生成器的作用!
可迭代对象是能够一次返回其成员的对象。这通常是通过for循环
完成的。像列表、元组、集合、字典、字符串等对象被称为可迭代对象。简而言之,任何可以循环遍历的对象都是可迭代的。可以使用for循环
逐个返回可迭代对象的元素。
迭代器是一个代表数据流的对象,即可迭代对象。它们实现了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迭代器的工作原理,可以更深入地了解如何从头开始创建一个迭代器,以便更好地理解事物的运作方式。将创建一个简单的迭代器来打印所有的偶数:
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循环
,它像以前一样工作。
生成器也是迭代器,但它们更加优雅。使用生成器,可以实现与迭代器相同的功能,但不需要在类中编写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()
生成器函数的对象时,它初始化了prev
和curr
变量。现在,当对象上调用next()
方法时,生成器函数计算值并返回输出,同时记住函数的状态。所以,下次调用next()
方法时,函数从上次停止的地方继续。
函数将在每次被next()
方法请求时继续生成值,直到prev
大于5,此时将引发StopIteration
错误,如下所示:
# 生成器直到抛出StopIteration错误
while True:
try:
print(next(fib_gen))
except StopIteration:
break
不必每次都编写函数来执行生成器。可以使用生成器表达式,就像列表推导式一样。唯一的区别是,与列表推导式不同,生成器表达式被括号包围,如下所示:
# 生成器表达式
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)