Lazy (惰性)
在 Python 中,“惰性(lazy)”指的是:
✅ 不立即计算或生成数据,而是在真正需要时才执行。
即:按需计算、延迟执行。
惰性 = 数据不是马上生成,而是用到时才生成。
例如:
def generate_items():
for i in range(10):
print("生成:", i)
yield i
调用:
gen = generate_items()
此时 没有打印任何东西。
只有当你开始取值时,才会生成:
next(gen)
输出:
生成: 0
这就是惰性。
如果数据有 100 万条,使用列表会把全部数据一次性加载:
lst = [i for i in range(1_000_000)]
巨大内存开销。
但生成器只在你取一个时生成一个:
gen = (i for i in range(1_000_000))
几乎不占内存。
程序不用等全部数据准备好,即可开始处理。
例如:
- 逐行读取大文件
- 网络流
- 分页数据库查询
- Kafka、Redis 队列消费
都是典型的“惰性”模型。
| 方式 | 惰性? | 举例 | 行为 |
|---|---|---|---|
| 生成器 | ✅ 是 | yield、(x for x in …) | 需要时才生成 |
| 列表推导式 | ❌ 否 | [x for x in …] | 一次性生成全部 |
| map、filter | ✅ Python3 是 | map(), filter() | 返回可迭代对象,不立即执行 |
| range() | ✅ | range(1000000) | 不创建列表,只保存边界 |
| list()、sum() | ❌ 否 | list(gen) | 一次性消耗 |
- 不提前占用内存
- 不立即执行逻辑
- 仅在使用元素时才执行/生成
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 中所有常见惰性(Lazy)对象与惰性机制清单」 —— 非常系统且实用,适合做速查表。
惰性(Lazy)= 不会立刻计算/生成数据,而是在需要时才进行计算。
⸻
| 类型 | 示例 | 描述 |
|---|---|---|
| 生成器函数 | def f(): yield x | 调用后返回生成器对象,按需生成值 |
| 生成器表达式 | (x*x for x in range(10)) | 语法类似列表推导式,但不会一次性生成全部数据 |
| 迭代器对象内的 next() 行为 | next(it) | 每次取一个,不会预先生成 |
可迭代对象(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) | 自定义迭代器 | 每次调用函数生成一个值 |
- 代表区间,不生成真实列表
- 访问元素时才计算
这整个模块都是“惰性的”,包括:
| 函数 | 说明 |
|---|---|
| count() | 无限计数序列 |
| cycle() | 无限循环 |
| repeat() | 重复一个对象 |
| islice() | 惰性切片 |
| chain() | 连接多个可迭代对象 |
| compress() | 根据掩码过滤 |
| accumulate() | 惰性累加 |
| takewhile() | 条件成立生成 |
| dropwhile() | 条件成立丢弃 |
itertools 是典型的惰性工具集。
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)
下面是一个 一次只生成一个值 的惰性对象:
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 才产生一个值
- 内存占用极低
def lazy_numbers(start, end):
while start < end:
yield start
start += 1
使用:
for n in lazy_numbers(0, 5):
print(n)
优点:
- 自动实现迭代器协议
- 写法更简洁
- 逻辑更清晰
当你希望一个属性第一次访问时计算,以后缓存:
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) # 第二次,读取缓存
更实际一点的:例如“股票数据流式获取”
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、日志流、消息队列
实现一个惰性的 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)” 的经典模式。
例如:
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 分页、数据库懒加载 |
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)。
改成这样:
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
这显然不符合预期。
真正模拟 range 的写法必须是:
- 第一次产出 start 本身
- 然后再增加 start
否则行为无法与 range 对齐。
yield 类似:
“把这个值给你,等你下一次来拿的时候,我再更新状态。”
所以必须:
- 先给当前值 (yield start)
- 再准备下一次的值 (start += 1)
如果顺序反了:
“我先更新再给你”,那第一次就错了。