推导式与生成器

推导式 (Comprehensions)

推导式是一种简洁的语法,可以从一个已有的可迭代对象(如列表、元组、集合等)中快速创建新的集合(列表、字典、集合)。

想象一下,你需要基于一个列表生成另一个新的列表,比如计算出0-9每个数字的平方。

传统的写法可能是这样的:

python
squares = []
for i in range(10):
    squares.append(i * i)

print(squares)
# 输出: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

代码清晰,但略显冗长。而推导式,就是为了让这种操作变得更加简洁和强大。


列表推导式

列表推导式 (List Comprehension)是最常用的一种推导式。它的基本语法是 [expression for item in iterable]

将上面的例子用列表推导式改写:

python
squares = [i * i for i in range(10)]
print(squares)
# 输出: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

一行代码就完成了同样的功能,是不是非常优雅?

列表推导式还可以加入 if 条件进行筛选:

python
# 只计算偶数的平方
even_squares = [i * i for i in range(10) if i % 2 == 0]
print(even_squares)
# 输出: [0, 4, 16, 36, 64]

列表推导式中的 if 只能做筛选,但如果想根据条件对元素进行不同的转换(类似 if-else),语法会有所不同。它的语法是 [value_if_true if condition else value_if_false for item in iterable]

python
# 将列表中的奇数变为 'odd', 偶数变为 'even'
numbers = [1, 2, 3, 4, 5]
results = ['even' if i % 2 == 0 else 'odd' for i in numbers]
print(results)
# 输出: ['odd', 'even', 'odd', 'even', 'odd']

注意if-else 结构要写在 for 循环的前面,而单独的 if 筛选条件要写在 for 循环的后面


集合推导式

集合推导式 (Set Comprehension)与列表推导式非常相似,只是使用花括号 {},它会自动处理重复元素。

python
numbers = [1, 2, 2, 3, 4, 4, 5]
unique_squares = {x * x for x in numbers}
print(unique_squares)
# 输出: {1, 4, 9, 16, 25} (顺序可能不同,因为集合是无序的)

字典推导式

与列表推导式类似,字典推导式 (Dictionary Comprehension)用于快速创建字典。语法是 {key_expression: value_expression for item in iterable}

python
# 创建一个数字及其平方的映射
square_map = {i: i * i for i in range(5)}
print(square_map)
# 输出: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 快速交换字典的键和值
my_dict = {'a': 1, 'b': 2, 'c': 3}
swapped_dict = {v: k for k, v in my_dict.items()}
print(swapped_dict)
# 输出: {1: 'a', 2: 'b', 3: 'c'}

嵌套推导式

推导式可以嵌套,用于处理更复杂的数据结构,例如将嵌套列表“扁平化”。

python
# 将一个嵌套列表展开成一个单一列表
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat_list = [num for row in matrix for num in row]
print(flat_list)
# 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]

这相当于两个 for 循环的嵌套,但形式上更为紧凑。


优点和注意事项

推导式的优点:

  • 简洁可读:代码更短,意图更明显。
  • 性能稍高:通常比等效的 for 循环 + .append() 速度更快,因为循环是在C语言层面实现的。

注意:推导式会一次性生成所有数据并加载到内存中。如果处理的数据量非常大,可能会消耗大量内存。这时,我们就需要“更懒惰”的工具——迭代器与生成器。


迭代器(Iterators)及其应用

迭代器 (Iterators):惰性的数据流。在我们谈论迭代器之前,需要先理解两个概念:

  • 可迭代对象 (Iterable):任何可以被 for 循环遍历的对象都是可迭代对象,例如列表、字符串、元组、字典等。从技术上讲,一个对象只要实现了 __iter__() 方法,它就是可迭代的。我们可以使用内置函数 iter() 从可迭代对象中获取一个迭代器。
  • 迭代器 (Iterator):它是一个表示数据流的对象。它实现了 __iter__()__next__() 两个方法。迭代器会记住当前遍历的位置,每次调用 __next__() 方法时,它会返回下一个值。当没有更多数据时,它会抛出 StopIteration 异常。一个关键点是:迭代器本身也是可迭代的,它的 __iter__ 方法通常返回它自己 (self)。

迭代器的核心优势在于“惰性求值”(Lazy Evaluation):它不会一次性将所有数据加载到内存中,而是在你需要时才生成下一个数据。这使得处理大数据集或无限序列成为可能,极大地节省了内存。


迭代器的使用

获取迭代器后,我们主要通过以下几种方式来消费(使用)它。

1. 使用 next() 函数逐个访问

这是最基本的使用方式,可以精确控制每一次迭代。

python
my_list = ['a', 'b', 'c']
my_iterator = iter(my_list)
print(next(my_iterator))  # 输出: 'a'
print(next(my_iterator))  # 输出: 'b'

2. 使用 for 循环(最常用)

for 循环是消费迭代器最自然、最常见的方式。它会自动调用 next() 并处理 StopIteration 异常。

python
# for 循环会自动处理 my_iterator 的剩余部分
for char in my_iterator:
    print(char) # 输出: 'c' (因为上面已经 next() 了两次)

# 对一个新的迭代器使用 for 循环
for char in iter(my_list):
    print(char) # 输出: a, b, c

3. 转换为其他集合类型

你可以使用 list(), tuple(), set() 等函数一次性消费掉迭代器中的所有剩余元素。

python
my_list = [10, 20, 30, 40]
my_iterator = iter(my_list)
next(my_iterator) # 跳过第一个元素
remaining_elements = list(my_iterator)
print(remaining_elements)  # 输出: [20, 30, 40]

注意:这种方式会失去迭代器的惰性求值优势,因为它会一次性将所有元素加载到内存中。

4. 应用场景示例:读取大文件

迭代器在处理大文件时非常有用。文件对象本身就是一个迭代器,它会逐行读取。

python
# 这样的代码不会消耗大量内存,因为它一次只处理一行
try:
    with open('large.log', 'r') as f:
        for line in f:
            if 'ERROR' in line:
                print(line.strip())
except FileNotFoundError:
    print("示例文件 'large.log' 不存在。")

for循环的本质

for 循环的背后,就是迭代器协议在工作。当你写 for item in my_iterable: 时,Python 内部大致会执行以下逻辑:

  1. 调用 iter(my_iterable) 获取一个迭代器对象。
  2. 进入一个无限循环,在循环内部: a. 调用这个迭代器对象的 __next__() 方法来获取下一个元素。 b. 如果成功获取,就执行 for 循环体内的代码。 c. 如果 __next__() 抛出 StopIteration 异常,就捕获这个异常并跳出循环。

手动模拟这个过程:

python
my_list = [10, 20, 30]
my_iterator = iter(my_list)
while True:
    try:
        item = next(my_iterator)
        print(f"获取到元素: {item}")
    except StopIteration:
        print("迭代结束!")
        break

itertools

Python 的标准库 itertools 模块提供了大量用于创建和操作迭代器的强大工具。它们本身都遵循迭代器协议,并使用 C 语言实现,因此具有极高的性能和内存效率,并且可以像积木一样自由组合。 这里介绍几个常用的函数:

  • itertools.count(start=0, step=1): 创建一个无限数字迭代器。
  • itertools.cycle(iterable): 对一个可迭代对象进行无限循环。
  • itertools.chain(*iterables): 将多个迭代器连接成一个更长的迭代器。
  • itertools.islice(iterable, stop): 对迭代器进行切片,返回的仍然是迭代器。
python
import itertools

# --- 1. itertools.count() 示例 ---
print("--- 1. itertools.count() 示例 ---")
# 创建一个从 101 开始的无限ID生成器
id_generator = itertools.count(101)
print(f"生成的第一个ID: {next(id_generator)}")
print(f"生成的第二个ID: {next(id_generator)}")

# 使用 zip 和 range 从无限迭代器中安全地获取有限个元素
print("生成接下来的5个ID:")
ids = [next(id_generator) for _ in range(5)]
print(ids)


# --- 2. itertools.cycle() 示例 ---
print("\n--- 2. itertools.cycle() 示例 ---")
# 创建一个循环播放器,用于交替分配任务
team_cycler = itertools.cycle(['A组', 'B组'])
print("为接下来6个任务分配组别:")
tasks = [f"任务{i}: {next(team_cycler)}" for i in range(1, 7)]
for task in tasks:
    print(task)


# --- 3. itertools.chain() 示例 ---
print("\n--- 3. itertools.chain() 示例 ---")
# 将不同类型的可迭代对象(列表、元组、字符串)连接起来
urgent_tasks = ['写报告', '开会']
normal_tasks = ('回复邮件', '整理文件')
minor_tasks = "打电话" # 字符串也是可迭代的

all_tasks = itertools.chain(urgent_tasks, normal_tasks, minor_tasks)
print("今天所有的任务按顺序是:")
print(list(all_tasks)) # ['写报告', '开会', '回复邮件', '整理文件', '打', '电', '话']


# --- 4. itertools.islice() 示例 ---
print("\n--- 4. itertools.islice() 示例 ---")
# islice 可以对任何可迭代对象进行切片,包括无限的
# 示例 a: 从无限计数器中获取第5到第9个元素 (索引从0开始)
print("从0开始计数,获取索引为5到9的数字:")
result_a = itertools.islice(itertools.count(), 5, 10)
print(list(result_a)) # [5, 6, 7, 8, 9]

# 示例 b: 对有限序列进行切片,并指定步长
print("从0-19的数字中,获取索引2到15,步长为3的数字:")
data = range(20)
result_b = itertools.islice(data, 2, 15, 3)
print(list(result_b)) # [2, 5, 8, 11, 14]

自定义迭代器

虽然通常使用生成器更方便,但我们也可以通过实现一个类来完全控制迭代行为。一个符合迭代器协议的类必须实现:

  • __iter__(): 返回迭代器对象本身 (return self)。
  • __next__(): 返回下一个值,并在结束时抛出 StopIteration
python
class MyRange:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < self.end:
            current_val = self.current
            self.current += 1
            return current_val
        else:
            raise StopIteration

for i in MyRange(0, 3):
    print(i) # 输出: 0, 1, 2

生成器 (Generators)

生成器 (Generators):创建迭代器的简单工厂。在大多数情况下,手动编写一个带有 __iter____next__ 的类是繁琐的。Python 提供了生成器,这是一种创建迭代器的更简单、更优雅的方式。

生成器是一种特殊的函数或表达式,它可以自动创建迭代器,无需我们手动编写迭代器类。

生成器函数 (yield)

一个函数只要包含了 yield 关键字,它就变成了一个 生成器函数 (Generator Function)。调用这个函数不会立即执行,而是返回一个生成器对象(它本身就是一个迭代器)。

yield 关键字非常神奇:

  • 它会“产出”一个值,就像 return 一样。
  • 但它不会终止函数,而是会“暂停”函数的执行,并保存当前的状态(包括局部变量)。
  • 当下一次对生成器调用 next() 时,函数会从上次暂停的地方继续执行。
python
def countdown(n):
    print("开始倒计时...")
    while n > 0:
        yield n
        n -= 1
    print("倒计时结束!")

c = countdown(3)
print(next(c))  # 输出: 开始倒计时... \n 3
print(next(c))  # 输出: 2

yield from 链接生成器

Python 3.3 引入的 yield from 语法可以简单地将多个生成器(或可迭代对象)串联起来,如同一个单一的生成器。

python
def gen_letters(): yield from "ABC"
def gen_numbers(): yield from [1, 2, 3]

def combined_generator():
    yield from gen_letters()
    yield from gen_numbers()

for item in combined_generator():
    print(item, end=' ')
# 输出: A B C 1 2 3 

生成器表达式

如果把列表推导式的 [] 换成 (),它就变成了一个 生成器表达式 (Generator Expression)。它看起来像推导式,但返回的是一个生成器对象,而不是一个列表,因此同样具有惰性求值的特性。

python
# 列表推导式: 立即创建列表,占用内存
list_comp = [i * i for i in range(1000)]

# 生成器表达式: 返回一个生成器,几乎不占内存
gen_expr = (i * i for i in range(1000))

# 生成器表达式非常适合作为函数的参数,尤其是像 sum(), max() 这样一次性消费序列的函数
# 这种方式非常高效,因为它不需要在内存中构建一个完整的列表
sum_of_squares = sum(i * i for i in range(1000))
print(sum_of_squares)

使用场景总结与对比

特性 推导式 (Comprehension) 迭代器 (Iterator) 生成器 (Generator)
核心思想 简洁地从序列创建新集合 一种访问集合元素的协议 创建迭代器的简单方式
数据生成 一次性生成 (Eager) 逐个生成 (Lazy) 逐个生成 (Lazy)
内存使用 占用内存,与数据量成正比 占用极少内存,与数据量无关 占用极少内存,与数据量无关
语法形式 [...], {...}, {...} 实现 __iter____next__ 的类 yield 函数或 (...) 表达式
使用场景 创建并需完整访问的小型集合 处理大数据集、无限序列、文件流等 需要自定义迭代逻辑,但又不想写完整迭代器类的场景

如何选择?

  • 优先考虑推导式:如果数据量不大,且你需要一个完整的列表/字典/集合来进行后续操作,推导式是最简洁、最Pythonic的选择。
  • 选择生成器处理大数据:当你处理一个巨大的文件、一个无穷的序列,或者任何不适合一次性载入内存的数据时,生成器是你的不二之选。它能让你以极低的内存成本实现复杂的迭代逻辑。
  • 直接使用迭代器:通常你不会直接去实现一个迭代器类,而是通过 iter() 函数或生成器来获取它。