Skip to main content
Documents
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Lazy (惰性)

什么是惰性

在 Python 中,“惰性(lazy)”指的是:

✅ 不立即计算或生成数据,而是在真正需要时才执行。

即:按需计算、延迟执行。

🔥 一句话解释

惰性 = 数据不是马上生成,而是用到时才生成。

🚀 典型代表:生成器(generator)

例如:

def generate_items():
    for i in range(10):
        print("生成:", i)
        yield i

调用:

gen = generate_items()

此时 没有打印任何东西。

只有当你开始取值时,才会生成:

next(gen)

输出:

生成: 0

这就是惰性。

☘️ 为什么要“惰性”?

1. 节省内存(最重要)

如果数据有 100 万条,使用列表会把全部数据一次性加载:

lst = [i for i in range(1_000_000)]

巨大内存开销。

但生成器只在你取一个时生成一个:

gen = (i for i in range(1_000_000))

几乎不占内存。

2. 加快启动速度

程序不用等全部数据准备好,即可开始处理。

3. 流式处理

例如:

  • 逐行读取大文件
  • 网络流
  • 分页数据库查询
  • Kafka、Redis 队列消费

都是典型的“惰性”模型。

🆚 惰性 vs 非惰性(立即执行)

方式 惰性? 举例 行为
生成器 ✅ 是 yield、(x for x in …) 需要时才生成
列表推导式 ❌ 否 [x for x in …] 一次性生成全部
map、filter ✅ Python3 是 map(), filter() 返回可迭代对象,不立即执行
range() range(1000000) 不创建列表,只保存边界
list()、sum() ❌ 否 list(gen) 一次性消耗

🧩 关键特征(记住这 3 条就懂惰性)

  1. 不提前占用内存
  2. 不立即执行逻辑
  3. 仅在使用元素时才执行/生成

🎯 示例:惰性读取大文件

def read_lines(fp):
    with open(fp) as f:
        for line in f:
            yield line.rstrip()

for 遍历时:

for line in read_lines("big.log"):
    print(line)

文件不会一次性读入内存,而是 按行读取(惰性)

📌 总结

Python 的惰性(Lazy Evaluation)指的是:只有在真正需要时才计算值或生成数据,从而节省内存、提高效率。典型案例包括生成器、range、map、filter,它们不会立即创建完整的数据结构,而是在迭代时逐个生成元素。


Python 中所有“惰性对象”清单

以下是一份 「Python 中所有常见惰性(Lazy)对象与惰性机制清单」 —— 非常系统且实用,适合做速查表。

✅ Python 中所有常见的“惰性对象”(Lazy Objects)

惰性(Lazy)= 不会立刻计算/生成数据,而是在需要时才进行计算。

🟦 一、生成器(Generators)—— 最典型的惰性对象

类型 示例 描述
生成器函数 def f(): yield x 调用后返回生成器对象,按需生成值
生成器表达式 (x*x for x in range(10)) 语法类似列表推导式,但不会一次性生成全部数据
迭代器对象内的 next() 行为 next(it) 每次取一个,不会预先生成

🟦 二、迭代器(Iterators)—— 大多是惰性的

可迭代对象(list/tuple/dict)不是迭代器,但 iter() 能创建迭代器。

常见惰性迭代器:

对象 示例 惰性行为
iter(list) iter([1,2,3]) 逐个返回元素,不复制
dict.keys() / values() / items() d.items() 动态视图,访问时才取值
file 对象 f = open(‘xx’); next(f) 一次读一行,不读取整个文件
enumerate enumerate([1,2,3]) 每次生成一个元组,不预生成所有项
zip zip(a, b) 并行逐项产生,不构建列表
map map(f, iterable) 每次调用函数生成一个值
filter filter(f, iterable) 惰性过滤,按需生成
reversed(iterator) 部分惰性 对迭代器是惰性的
iter(callable, sentinel) 自定义迭代器 每次调用函数生成一个值

🟦 三、标准库中的惰性对象(重要)

1)range()

  • 代表区间,不生成真实列表
  • 访问元素时才计算

2)itertools 模块

这整个模块都是“惰性的”,包括:

函数 说明
count() 无限计数序列
cycle() 无限循环
repeat() 重复一个对象
islice() 惰性切片
chain() 连接多个可迭代对象
compress() 根据掩码过滤
accumulate() 惰性累加
takewhile() 条件成立生成
dropwhile() 条件成立丢弃

itertools 是典型的惰性工具集。

🟦 四、异步惰性对象(Async Lazy Objects)

Python 3.6+ 引入了异步迭代器,也天然是惰性的。

| 对象类别 | 示例 | 描述 | | — | — | | 异步生成器函数 | async def gen(): yield x | 按需 await 生成 | | aiter / anext | aiter(obj) | 异步惰性迭代 | | async for | 异步流式处理数据 |

常用于 I/O 流式数据。

🟦 五、其他具有“惰性行为”的结构

对象 示例 惰性点
memoryview memoryview(b) 不复制数据,按需访问
某些 NumPy array views arr[::2] 视图不复制,仅惰性引用
pandas iterrows / itertuples 惰性产生行数据
日志模块的惰性格式化 logger.debug(“x=%s”, x) 不到 debug 级别不格式化

🟦 六、哪些不是惰性的?

对比一下:

类型 是否惰性 原因
list/tuple/set/dict 马上构建全部内容
list comprehension 一次性生成所有元素
set/dict comprehension 同上
sorted() 立即排序返回新列表
read() 读文件 一次读完

🟧 总结(速查版)

✅ 惰性:按需

generator, iterator, range, map, filter, zip, enumerate, yield, yield from, itertools.*

✅ 异步惰性

async generator, aiter, anext, async for

✅ 特殊惰性

memoryview, numpy/pandas 视图型操作

❌ 非惰性

list/tuple/set/dict + comprehension


如何自己实现一个“惰性对象”

以下是一套 从简单 -> 进阶 的指南,教你如何在 Python 中 自己实现一个“惰性对象”(Lazy Object)。

内容非常实用,适用于你任何项目,例如数据查询、分页、网络请求、流式处理等。

✅ 一、什么叫“惰性对象”?

惰性对象 = 不立即计算,等到需要时才计算。

在 Python 中:

  • 需要时才生成数据 -> 惰性生成(lazy generation)
  • 需要时才计算结果 -> 惰性计算(lazy evaluation)

🟦 二、最简单的:用 __iter__ + __next__ 自定义惰性迭代器

下面是一个 一次只生成一个值 的惰性对象:

class LazyCounter:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.end:
            raise StopIteration
        value = self.current
        self.current += 1
        return value

使用:

counter = LazyCounter(0, 5)

for x in counter:
    print(x)

特点:

  • 不生成 list
  • 每次 next 才产生一个值
  • 内存占用极低

🟦 三、用生成器更简单:yield(惰性生成的推荐方式)

def lazy_numbers(start, end):
    while start < end:
        yield start
        start += 1

使用:

for n in lazy_numbers(0, 5):
    print(n)

优点:

  • 自动实现迭代器协议
  • 写法更简洁
  • 逻辑更清晰

🟦 四、惰性属性(Lazy Property)

当你希望一个属性第一次访问时计算,以后缓存:

class LazyObject:
    @property
    def heavy_result(self):
        print("Running heavy computation ...")
        return sum(i*i for i in range(10_000_000))

但这样每次访问都重新计算。我们让它变成 惰性且缓存

class LazyObject:
    @property
    def heavy_result(self):
        if not hasattr(self, "_cached"):
            print("Running heavy computation ...")
            self._cached = sum(i*i for i in range(10_000_000))
        return self._cached

用法:

obj = LazyObject()
print(obj.heavy_result)  # 第一次,执行计算
print(obj.heavy_result)  # 第二次,读取缓存

🟦 五、惰性对象类:按需从数据库 / API 取数据

更实际一点的:例如“股票数据流式获取”

class StockLazyFetcher:
    def __init__(self, fetch_func):
        self.fetch_func = fetch_func
        self.buffer = []
        self.finished = False

    def __iter__(self):
        return self

    def __next__(self):
        if not self.buffer and not self.finished:
            chunk = self.fetch_func()  # 一次从 API 拿一批
            if not chunk:
                self.finished = True
            else:
                self.buffer.extend(chunk)

        if not self.buffer:
            raise StopIteration

        return self.buffer.pop(0)

使用:

fetcher = StockLazyFetcher(fetch_api)
for stock in fetcher:
    print(stock)

益处:

  • 不会一次性加载所有数据
  • 减少 API 压力、数据库压力
  • 特别适合分页 API、日志流、消息队列

🟦 六、惰性序列(LazySequence):只在访问时计算

实现一个惰性的 Fibonacci:

class LazyFibo:
    def __init__(self):
        self.cache = [0, 1]

    def __getitem__(self, n):
        while len(self.cache) <= n:
            self.cache.append(self.cache[-1] + self.cache[-2])
        return self.cache[n]

使用:

f = LazyFibo()

print(f[10])   # 第一次计算
print(f[50])   # 自动算到 50
print(f[10])   # 直接使用缓存

这是 “惰性 + 缓存(memoization)” 的经典模式。

🟦 七、惰性包装器(Lazy Wrapper)——让任意函数变惰性

例如:

class Lazy:
    def __init__(self, func, *args, **kwargs):
        self.func = func
        self.args = args
        self.kwargs = kwargs
        self._value = None
        self._done = False

    def value(self):
        if not self._done:
            self._value = self.func(*self.args, **self.kwargs)
            self._done = True
        return self._value

使用:

import time

def slow_add(a, b):
    time.sleep(2)
    return a + b

lazy_result = Lazy(slow_add, 3, 5)

print("not executed yet")
print(lazy_result.value())  # 2秒计算
print(lazy_result.value())  # 立即返回

🟦 八、总结(速查版)

惰性类型 示例 适用场景
惰性迭代器 __iter__ + __next__ 流式数据、无限序列
生成器 yield 最常用惰性生成
惰性属性 lazy @property 重计算缓存
惰性序列 __getitem__ 无限数组、数学序列
惰性包装器 Lazy(func) 延迟执行复杂逻辑
数据流惰性拉取 fetcher API 分页、数据库懒加载

Next

def lazy_numbers(start, end):
    while start < end:
        yield start
        start += 1

start += 1 为什么要放在 yield start 之后?

start += 1 放在 yield start 之后,是为了保证生成的序列顺序正确、值不会跳过。

下面给你详细解释:

✅ 结论(一句话)

yield start先返回当前值,然后 start += 1 再让下次迭代返回下一个值。

如果你把 start += 1 放前面,会导致少一个值 / 第一个值丢失

✅ 为什么要写在后面?(逐行流程)

以你的代码为例:

def lazy_numbers(start, end):
    while start < end:
        yield start   # ① 把当前 start 返回给调用者
        start += 1    # ② 更新 start,下一轮会返回新值

如果 start=0 end=3:

循环 yield 返回值 更新后 start
第1次 0 1
第2次 1 2
第3次 2 3 -> while 结束

生成:0, 1, 2

行为完全符合 range(start, end)。

❌ 如果 start += 1 放在 yield 前面?

改成这样:

def lazy_numbers(start, end):
    while start < end:
        start += 1
        yield start

再看流程:

start=0, end=3

循环 start+=1 后 yield 返回值
第1次 1 1
第2次 2 2
第3次 3 while 终止(不 yield)

结果:1, 2

  • 👉 0 被跳过了
  • 👉 3 没有被 yield

这显然不符合预期。

📌 和 for i in range(start, end) 一样的逻辑

真正模拟 range 的写法必须是:

  • 第一次产出 start 本身
  • 然后再增加 start

否则行为无法与 range 对齐。

🎯 最通俗的解释

yield 类似:

“把这个值给你,等你下一次来拿的时候,我再更新状态。”

所以必须:

  1. 先给当前值 (yield start)
  2. 再准备下一次的值 (start += 1)

如果顺序反了:

“我先更新再给你”,那第一次就错了。