Python中:可迭代对象、迭代器、生成器、生成器表达式、列表推导式

0 阅读8分钟

迭代器(Iterator)和生成器(Generator)

迭代器和生成器的关系:生成器是迭代器的“快捷方式”。你不需要写繁琐的类和 __next__ 方法,只需要用一个特殊的关键字:yield

是什么:简单来说,迭代器(Iterator) 是让你能一个接一个访问元素的对象,而生成器(Generator) 则是创建这些迭代器的一种更优雅、更省内存的高级方式。

作用:节省内存。如果你有一个包含 1 亿个数字的列表,它会瞬间吃光你的 RAM。但迭代器不需要在内存中存储所有数字,它只记住当前的位置,每次调用时计算出下一个值。

它们都采用 “惰性求值”(Lazy Evaluation),只有在你要的时候才计算。

代码实现:迭代器

class MyCounter:
    def __init__(self, low, high):
        self.current = low
        self.high = high

    def __iter__(self):
        # 迭代器协议要求返回对象本身
        return self

    def __next__(self):
        # 每次调用 next() 都会跑这里的逻辑
        if self.current <= self.high:
            value = self.current
            self.current += 1
            return value
        else:
            # 没有数据了,必须抛出这个异常,否则 for 循环不会停止
            raise StopIteration

# 使用迭代器
counter = MyCounter(1, 3)

print(next(counter)) # 输出 1
print(next(counter)) # 输出 2
print(next(counter)) # 输出 3
# print(next(counter)) # 再次调用会触发 StopIteration

代码实现:生成器函数

场景:实现斐波那契数列(Fibonacci) 这是一个经典的案例,因为斐波那契数列可以是无限的,用生成器处理最节省内存。

生成器用 yield 代替了繁琐的类定义。它会自动帮我们处理 __iter____next__StopIteration

def fibonacci_gen(n):
    """生成前 n 个斐波那契数"""
    a, b = 0, 1
    count = 0
    while count < n:
        yield a  # 产出值,并在此处“暂停”
        a, b = b, a + b
        count += 1

# 使用生成器
fib = fibonacci_gen(5)

print(type(fib)) # <class 'generator'>

for num in fib:
    print(num) # 依次输出 0, 1, 1, 2, 3

代码讲解

代码中没有return,它是怎么返回值的?难道是yield返回了值吗? 是的。yield 的确返回了值,但它和 return 的工作方式完全不同。 return:直接返回所有结果,如果数据量巨大内存将会爆炸。 yield:只有当你需要值的时候才会返回,比如for循环或者手动调用next。

代码执行的微观过程:

让我们看看你的代码在 for num in fib 时发生了什么:
1、第一次循环: 进入函数,a=0, b=1。碰到 yield 0。函数暂停,把 0 传给外面的 num。打印 0
2、第二次循环:yield a下一行开始:计算 a, b = 1, 1count = 1。回到循环顶部,碰到 yield 1。函数暂停,把 1 传给 num。打印 1
3、以此类推...
4、最后:count < n 不再成立时,函数自然结束。for 循环感知到函数结束了,就会优雅地停止,而不会报错。

为什么要这么麻烦?(核心优势):
如果我们要生成前 100 万个斐波那契数:
return 你必须先创建一个包含 100 万个数字的列表,这会瞬间吃掉几百 MB 的内存
yield 内存中始终只保存 a, b, count 这三个变量。无论生成多少个数,内存占用几乎为零

yield和return对比

return vs yield 深度对比

特性return (普通函数)yield (生成器函数)
执行状态执行到此行立即终止函数。执行到此行暂停函数,保留现场。
返回值返回一个具体的值或对象。返回一个生成器对象(迭代器)。
内存占用必须一次性生成所有结果(大列表)。按需生成,一次只占一个元素的内存。
变量生命周期函数退出后,局部变量被销毁局部变量状态被封存,下次从断点继续。
调用方式调用一次执行一次完整逻辑。配合 for 循环或 next() 多次获取值。

生成器表达式和列表推导式

生成器最“轻量化”的形式:生成器表达式(Generator Expression)

它和列表推导式(List Comprehension)长得几乎一模一样,唯一的区别是:列表推导式用方括号 [],而生成器表达式用圆括号 ()

但在内存的世界里,这一个小小的括号差别,就是“倾家荡产”与“勤俭持家”的区别。

1、内存大比拼:[] vs ()

假设我们要处理 100 万个 数字的平方:

import sys
​
# 1. 列表推导式:直接在内存里创建 100 万个元素的列表
list_comp = [x**2 for x in range(1000000)]
print(f"列表占用内存: {sys.getsizeof(list_comp)} 字节") 
​
# 2. 生成器表达式:只创建了一个“生成规则”
gen_exp = (x**2 for x in range(1000000))
print(f"生成器占用内存: {sys.getsizeof(gen_exp)} 字节")

内存占用: 列表推导式:8448728 字节(约8MB) 生成器表达式:208 字节

既然你感兴趣,那我们就来看看生成器最“轻量化”的形式:生成器表达式(Generator Expression)

它和列表推导式(List Comprehension)长得几乎一模一样,唯一的区别是:列表推导式用方括号 [],而生成器表达式用圆括号 ()

但在内存的世界里,这一个小小的括号差别,就是“倾家荡产”与“勤俭持家”的区别。


1. 内存大比拼:[] vs ()

假设我们要处理 100 万个 数字的平方:

import sys

# 1. 列表推导式:直接在内存里创建 100 万个元素的列表
list_comp = [x**2 for x in range(1000000)]
print(f"列表占用内存: {sys.getsizeof(list_comp)} 字节") 

# 2. 生成器表达式:只创建了一个“生成规则”
gen_exp = (x**2 for x in range(1000000))
print(f"生成器占用内存: {sys.getsizeof(gen_exp)} 字节")

结果对比:

方式内存占用(典型值)状态
列表 [ ]约 8,000,000 字节 (8MB)瞬间塞满内存,数据都在那儿。
生成器 ( )约 100 字节几乎不占内存,数据“随叫随到”。

2. 为什么生成器表达式这么神奇?

生成器表达式本质上是 yield 函数的缩写版。 当你写下 (x**2 for x in range(10)) 时,Python 在后台其实为你做了一个类似这样的工作:

def __hidden_generator():
    for x in range(10):
        yield x**2

gen_exp = __hidden_generator()

3. 什么时候用生成器表达式?

  • 数据处理管道:比如读取一个巨大的 CSV 文件,过滤掉无效行,然后转换数据。你可以用一连串的生成器表达式,数据会像流水一样流过这些“滤芯”,而不会堵塞内存。

  • 聚合运算:如果你只是想算个总和 sum()、最大值 max(),完全没必要先存成列表。

# 优雅且省内存的写法
total_sum = sum(x**2 for x in range(1000000))

可迭代对象

1. 是什么?

是什么:可以使用for遍历的对象就是可迭代对象。

实现:简单来说,只要一个对象实现了 __iter__() 魔术方法(或者实现了 __getitem__() 且支持整数索引),它就是可迭代对象。 这意味着它承诺了: “你可以用 for 循环来遍历我。”

常见的可迭代对象:

  • 序列类型list (列表), str (字符串), tuple (元组), bytes
  • 非序列类型dict (字典), set (集合)
  • 文件对象
  • 生成器

2. 可迭代对象 vs 迭代器

这是最容易混淆的地方。请记住这个公式:

可迭代对象 (Iterable) iter()\xrightarrow{iter()} 迭代器 (Iterator)

特性可迭代对象 (Iterable)迭代器 (Iterator)
魔法方法只有 __iter__必须有 __iter____next__
能否 next()不能直接调用 next()可以调用 next()
例子[1, 2, 3]iter([1, 2, 3])

通过 iter() 函数把可迭代对象变成迭代器

如上公式,可以使用 iter() 函数把可迭代对象变成迭代器。

my_list = [10, 20]
# 1. 获取迭代器
it = iter(my_list)

# 2. 模拟循环
try:
    print(next(it)) # 10
    print(next(it)) # 20
    print(next(it)) # 这里会抛出 StopIteration
except StopIteration:
    print("遍历结束!")

这里模拟for的过程,如果再在代码中加入一个for的话,那么就会跳过奇数个输出,因为for遍历一个,next又遍历了一个。

这也就是为什么不让 List 直接当 Iterator的原因:

  • 如果你让 List 自带 next(),那么当你进行嵌套循环(如双重 for 循环)时,内层循环会直接消耗掉外层循环的进度,导致逻辑混乱。
  • 通过 iter() 产生独立的迭代器,意味着你可以对同一个列表同时开启多个独立的遍历进度。

可迭代对象、迭代器、生成器

在 Python 的世界里,关系是这样的:

  • 可迭代对象 (Iterable) :比如 list, str,你可以对它用 for 循环。
  • 迭代器 (Iterator) :是一个有“车头”的对象,你调用 next() 它就走一步。
  • 生成器 (Generator) :是一种特殊的迭代器。所有的生成器都是迭代器,但迭代器不一定是生成器。

生成器(Generator)是迭代器(Iterator)的“语法糖”和进化版。在 95% 的实际开发场景中,我们都会优先使用生成器。

要实现一个标准的迭代器,你必须手动写一个类,并实现两个特定方法:

  1. __iter__(): 返回对象本身。
  2. __next__(): 决定下一次给什么值,并在结束时手动抛出 StopIteration 异常。

生成器只需要一个带有 yield 的普通函数。