数据科学经常涉及处理那些庞大到无法轻松载入内存的数据集。一次性加载整个数据集可能效率低下,甚至不可能实现。Python 提供了强大的结构,称为迭代器和生成器,以优雅地处理这类情况,从而按顺序处理数据,而无需预先存储所有内容。这种方法对于编写可扩展且内存效率高的数据分析和机器学习管道代码非常重要。了解迭代器本质上,迭代器是一个表示数据流的对象。它允许你逐个遍历一系列项。Python 中迭代的机制受迭代器协议控制,该协议要求对象实现两个特定方法:__iter__(): 此方法返回迭代器对象本身。当你开始迭代一个对象时(例如,在 for 循环的开头),就会调用此方法。__next__(): 此方法返回序列中的下一个项。当没有更多项时,它会引发 StopIteration 异常。许多内置的 Python 对象,如列表、元组、字典和字符串,都是可迭代的。这意味着它们有一个返回迭代器的 __iter__() 方法。你可以使用内置的 iter() 函数从可迭代对象获取迭代器,并使用 next() 函数检索下一个项。让我们通过一个简单的列表来看看它的实际作用:my_list = [10, 20, 30] # 从列表中获取一个迭代器对象 my_iterator = iter(my_list) # 检查类型 print(type(my_iterator)) # 输出: <class 'list_iterator'> # 使用 next() 逐个检索项 print(next(my_iterator)) # 输出: 10 print(next(my_iterator)) # 输出: 20 print(next(my_iterator)) # 输出: 30 # 尝试获取另一个项会引发 StopIteration try: print(next(my_iterator)) except StopIteration: print("没有更多项了!") # 输出: 没有更多项了!你很少需要像这样直接调用 iter() 和 next()。Python 的 for 循环会在幕后自动处理这个过程。当你编写:for item in my_list: print(item)Python 首先调用 iter(my_list) 来获取一个迭代器。然后,在每次迭代中,它对迭代器调用 next() 以获取下一个项并将其赋值给 item。当 next() 引发 StopIteration 时,循环会自动停止。这使得迭代清晰直观。介绍生成器:创建迭代器的一种更简单的方法虽然你可以通过定义一个带有 __iter__ 和 __next__ 方法的类来创建自定义迭代器,但 Python 提供了一种更方便的方式:生成器。生成器是一种特殊的函数,它返回一个迭代器。生成器函数不使用 return 返回值并退出,而是使用 yield 关键字。当生成器函数遇到 yield 时,它会在此处暂停执行,将生成的值返回给调用者,并保存其局部状态。下次调用迭代器的 next() 方法时,函数会从 yield 语句之后恢复执行,一直持续到遇到另一个 yield、完成或遇到 return 语句(这会隐式引发 StopIteration)。考虑这个简单的生成器函数:def count_up_to(n): """生成从 1 到 n 的数字。""" i = 1 while i <= n: yield i # 在这里暂停,返回 i,并等待 i += 1 # 在下次调用 next() 时从这里恢复 print("生成器已完成。") # 这行在最后一个 yield 之后运行 # 创建一个生成器对象(调用函数本身并不会立即执行它) counter_gen = count_up_to(3) print(type(counter_gen)) # 输出: <class 'generator'> # 使用 next() 迭代 print(next(counter_gen)) # 输出: 1 print(next(counter_gen)) # 输出: 2 print(next(counter_gen)) # 输出: 3 # 下次调用会引发 StopIteration try: next(counter_gen) except StopIteration: print("按预期捕获到 StopIteration。") # 输出: 生成器已完成。 # 输出: 按预期捕获到 StopIteration。 # 你也可以直接使用 for 循环 print("\n使用 for 循环:") for number in count_up_to(4): print(number) # 输出: # 使用 for 循环: # 1 # 2 # 3 # 4 # 生成器已完成。请注意,“Generator finished.”消息只会在循环(或手动 next() 调用)用尽所有生成的值之后才会出现。为什么在数据科学中使用生成器?生成器在数据科学和机器学习中特别有用,原因如下:内存效率: 这是最重要的优势。生成器一次只产生一个项,并且仅在请求时产生(惰性评估)。它们不会将整个序列存储在内存中。想象一下处理一个几 GB 大小的日志文件。使用生成器逐行读取它是可行的,而将整个文件加载到列表中则很可能会导致程序崩溃。# 示例:逐行处理可能很大的文件 def read_large_file(filepath): """逐行读取文件的生成器。""" try: with open(filepath, 'r') as f: for line in f: yield line.strip() # 一次处理/生成一行 except FileNotFoundError: print(f"错误: 未在 {filepath} 找到文件") # 用法: # for log_entry in read_large_file('huge_log_file.txt'): # if 'ERROR' in log_entry: # print(f"找到错误: {log_entry}") # 这会在不将整个文件加载到内存中的情况下处理文件。惰性评估: 计算仅在需要值时执行。如果你为一个可能无限长或非常长的序列创建生成器,你只需为实际使用的项支付计算成本。可组合性: 生成器可以串联起来,以创建高效的数据处理管道。一个生成器的输出可以作为下一个生成器的输入,数据以每次一个项的方式流经管道。def get_lines(filepath): """从文件中生成行。""" with open(filepath, 'r') as f: for line in f: yield line def extract_values(lines, column_index): """从特定列生成值(假设为 CSV)。""" for line in lines: parts = line.strip().split(',') if len(parts) > column_index: yield parts[column_index] def convert_to_float(values): """生成浮点数转换,跳过错误。""" for value in values: try: yield float(value) except ValueError: continue # 跳过非浮点数值 # 示例管道(假设 'data.csv' 存在) # file_path = 'data.csv' # lines_gen = get_lines(file_path) # values_gen = extract_values(lines_gen, 2) # 获取第3列 # floats_gen = convert_to_float(values_gen) # 现在你可以逐个处理浮点数 # total = 0 # count = 0 # for val in floats_gen: # total += val # count += 1 # if count > 0: # print(f"第2列的平均值: {total / count}")生成器表达式正如在上一节关于推导式中简要提及的,Python 提供了一种简洁的语法来即时创建简单的生成器:生成器表达式。它们看起来非常像列表推导式,但使用圆括号 () 而不是方括号 []。# 列表推导式(在内存中创建完整列表) squares_list = [x * x for x in range(10)] print(squares_list) # 输出: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] print(type(squares_list)) # 输出: <class 'list'> # 生成器表达式(创建一个生成器对象) squares_gen = (x * x for x in range(10)) print(squares_gen) # 输出: <generator object <genexpr> at 0x...>(地址可能不同) print(type(squares_gen)) # 输出: <class 'generator'> # 你需要迭代才能获取值 print(list(squares_gen)) # 转换为列表以查看值 # 输出: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] # 注意:一旦迭代,生成器就会被耗尽 print(list(squares_gen)) # 输出: []当你需要一个迭代器立即使用(例如在 for 循环中或传递给 sum() 等函数),并且逻辑足够简单以适应单个表达式时,生成器表达式非常有用。它们提供与生成器函数相同的内存优势。# 在不创建中间列表的情况下求平方和 total_sum_of_squares = sum(x * x for x in range(1000000)) print(f"高效计算的总和: {total_sum_of_squares}")通过理解并运用迭代器和生成器,你可以编写高效处理数据流和大型序列的 Python 代码,这是为机器学习模型准备和处理数据时的一项重要能力。它们通过将数据生成逻辑与数据消费逻辑分离,从而促进更简洁的代码。