推导式 (Comprehensions)
推导式是一种简洁的语法,可以从一个已有的可迭代对象(如列表、元组、集合等)中快速创建新的集合(列表、字典、集合)。
想象一下,你需要基于一个列表生成另一个新的列表,比如计算出0-9每个数字的平方。
传统的写法可能是这样的:
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]
。
将上面的例子用列表推导式改写:
squares = [i * i for i in range(10)]
print(squares)
# 输出: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
一行代码就完成了同样的功能,是不是非常优雅?
列表推导式还可以加入 if
条件进行筛选:
# 只计算偶数的平方
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]
。
# 将列表中的奇数变为 '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)与列表推导式非常相似,只是使用花括号 {}
,它会自动处理重复元素。
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}
。
# 创建一个数字及其平方的映射
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'}
嵌套推导式
推导式可以嵌套,用于处理更复杂的数据结构,例如将嵌套列表“扁平化”。
# 将一个嵌套列表展开成一个单一列表
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()
函数逐个访问
这是最基本的使用方式,可以精确控制每一次迭代。
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
异常。
# 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()
等函数一次性消费掉迭代器中的所有剩余元素。
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. 应用场景示例:读取大文件
迭代器在处理大文件时非常有用。文件对象本身就是一个迭代器,它会逐行读取。
# 这样的代码不会消耗大量内存,因为它一次只处理一行
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 内部大致会执行以下逻辑:
- 调用
iter(my_iterable)
获取一个迭代器对象。 - 进入一个无限循环,在循环内部:
a. 调用这个迭代器对象的
__next__()
方法来获取下一个元素。 b. 如果成功获取,就执行for
循环体内的代码。 c. 如果__next__()
抛出StopIteration
异常,就捕获这个异常并跳出循环。
手动模拟这个过程:
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)
: 对迭代器进行切片,返回的仍然是迭代器。
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
。
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()
时,函数会从上次暂停的地方继续执行。
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
语法可以简单地将多个生成器(或可迭代对象)串联起来,如同一个单一的生成器。
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)
。它看起来像推导式,但返回的是一个生成器对象,而不是一个列表,因此同样具有惰性求值的特性。
# 列表推导式: 立即创建列表,占用内存
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()
函数或生成器来获取它。