Python 生成器和 Yield 解释:通过真实示例节省内存

  1. 首页
  2. 博客
  3. 软件开发

软件开发

Python 生成器和 Yield 解释:通过真实示例节省内存

生成器一次生成一个值,而不是在内存中构建整个列表。准确观察yield如何暂停函数,查看我们测量的内存差异,并构建一个小型数据管道。

阅读约 9 分钟
Python生成器和yield解释:用真实例子节省内存

要点速览

  • 包含 yield 的函数返回一个 生成器,并且其主体在您请求值之前不会运行。
  • yield 暂停该函数并保持其本地状态直到下一个请求。
  • 无论生成器产生多少值,都会使用少量固定的内存。
  • 一台发电机只能消耗一次,这是最常见的陷阱。

想象一下将 10 GB 日志文件加载到列表中只是为了查找错误。您的程序可能在找到内存之前就耗尽了内存。 Python 生成器 通过按需生成值(一次一个)来解决这个问题,这样您就可以在几乎没有内存的情况下处理大量甚至无穷无尽的数据。

本指南逐步展示了发电机运行时实际发生的情况。我们在 Python 3.13 上运行每个示例,并且输出来自该运行。它有助于熟悉函数和循环,我们的 Python 初学者教程 对此进行了介绍。

什么是发电机?

生成器是一种特殊类型的迭代器,您可以通过使用 yield 编写函数来创建它。任何带有 yield 的函数都会自动成为生成器函数,调用它会为您提供一个生成器对象,而不是运行代码。

def count_up(limit):
    print("starting")
    n = 1
    while n <= limit:
        yield n
        n += 1
    print("finished")

gen = count_up(2)
print(gen)

# <generator object count_up at 0x...>

请注意,“starting”没有打印出来。调用该函数仅创建了生成器。里面还没有运行任何东西。

产量如何运作,一步一步

每次调用 next() 都会运行该函数,直到到达 yield,交回该值并冻结到位。下一个调用将在该行之后立即恢复,并且每个局部变量都完好无损。

print(next(gen))
# starting
# 1

print(next(gen))
# 2

next(gen)
# finished
# StopIteration

仔细遵循输出。 “starting”仅出现在第一个 next() 上。在第二个,执行从停止的地方继续,因此 n 仍然可用并变为 2。在第三个,循环结束,“finished”打印,函数从末尾脱落,Python 引发 StopIteration 表示没有剩下任何东西。

您很少自己调用 next()。 for 循环会为您完成此操作并安静地处理 StopIteration,list()、sum() 和类似函数也是如此:

print(list(count_up(3)))

# starting
# finished
# [1, 2, 3]

收益率与回报

方面 返回 产量
它的作用 结束函数 暂停功能
产生的价值 一项结果 一个系列,一次一个
当地州 废弃 值之间保留
当代码运行时 接到电话后立即 仅当请求值时
许多值的记忆 必须把他们全部抱住 一次持有一个

这种延迟被称为惰性求值:仅在需要时才计算值。生成器内的普通 return 只是结束它,从而触发 StopIteration。

测量的内存差异

我们使用 sys.getsizeof 将一百万个方格的列表与等效生成器进行了比较:

import sys

squares_list = [n * n for n in range(1_000_000)]
squares_gen = (n * n for n in range(1_000_000))

print(sys.getsizeof(squares_list), sys.getsizeof(squares_gen))

# 8448728 200

列表本身的结构占用了大约 8.4 MB,而生成器在我们的系统上只占用了 200 字节。真正的差距更大,因为 getsizeof 没有计算列表内的整数对象。对于十亿个项目,生成器的大小也保持不变,因为它只存储其当前状态,而不存储其结果。

不过,这需要权衡。列表可以通过 len() 进行重用、索引和测量。生成器不提供这些。如果您需要随机访问,或者需要对少量数据进行多次循环,则列表会更简单。有关这些类型的更多信息,请参阅我们的 Python 列表、元组与集合指南。

生成器表达式

对于简单的情况,您不需要函数。生成器表达式看起来像 列表理解,用圆括号代替方括号:

squares = [n * n for n in range(5)]   # builds a list now
squares = (n * n for n in range(5))   # produces values on demand

print(sum(n * n for n in range(5)))

# 30

当生成器表达式是函数的唯一参数时(如 sum() 中所示),您可以省略多余的括号。对于大数据的总计来说,这是一个整洁的习惯,因为不会创建临时列表。

构建数据管道

这就是发电机真正发挥作用的地方。小型生成器可以链接起来,以便每个项目一次一个地流经每一步:

def read_lines(lines):
    for line in lines:
        yield line.strip()

def only_errors(lines):
    for line in lines:
        if line.startswith("ERROR"):
            yield line

def messages(lines):
    for line in lines:
        yield line.split(" ", 1)[1]

log = ["INFO started\n", "ERROR db down\n",
       "ERROR disk full\n", "INFO stopped\n"]

print(list(messages(only_errors(read_lines(log)))))

# ['db down', 'disk full']

每个步骤只完成一项小工作,并且可以单独进行测试。在实际使用中,请将示例列表交换为打开的文件,因为文件对象可以逐行读取,如我们的 Python 文件处理指南 所示。即使是多千兆字节的日志也可以通过很少的内存。

提示: 管道在被某些东西消耗之前不会工作。建立链条是即时的。仅当您循环最终生成器或将其包装在像 list() 这样的调用中时,才会进行处理。

无限序列

列表不可能是无限的,但生成器可以,因为它只计算下一个值。使用 itertools.islice 获取所需数量:

import itertools

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

print(list(itertools.islice(fibonacci(), 8)))

# [0, 1, 1, 2, 3, 5, 8, 13]

不过要小心:无限制地调用 list(fibonacci()) 会一直运行,直到你的内存耗尽为止。相同的模式适用于 ID 生成器、轮询循环和分页 API 结果,仅当当前页面用完时才获取下一页。

产量和批次

yield from 交给另一个可迭代对象,为您节省一个嵌套循环:

def chain_all(*groups):
    for group in groups:
        yield from group

print(list(chain_all([1, 2], (3, 4))))

# [1, 2, 3, 4]

一种非常实用的模式是将数据拆分为固定大小的批次,例如一次向数据库中插入一千行:

def batches(items, size):
    batch = []
    for item in items:
        batch.append(item)
        if len(batch) == size:
            yield batch
            batch = []
    if batch:
        yield batch

print(list(batches(range(7), 3)))

# [[0, 1, 2], [3, 4, 5], [6]]

最后的if batch确保不会丢失较小的剩余组。 Python 3.12 及更高版本还包括 itertools.batched,它执行类似的工作。

发送、关闭和清理

生成器也可以接收值。 send() 恢复生成器,并且该值成为 yield 表达式的结果。您必须首先调用 next() 一次才能到达第一个 yield:

def running_total():
    total = 0
    while True:
        value = yield total
        total += value

t = running_total()
next(t)
print(t.send(10), t.send(5))

# 10 15

大多数人很少需要这个。更常见的问题是清理。如果提前停止,当生成器关闭时 finally 块仍然运行:

def tracked():
    try:
        yield 1
        yield 2
    finally:
        print("cleanup ran")

tr = tracked()
next(tr)
tr.close()

# cleanup ran

Pytest 使用了这个想法:带有 yield 的固定装置随后运行其清理,如我们的 pytest 教程 所示。同样,标准库的 contextmanager 装饰器将生成器变成 with 块。

类中的生成器

将 __iter__ 方法编写为生成器是使您的 自己的类 可迭代的最简单方法。一个好处是每个循环都会获得一个新的生成器,因此该对象可以一次又一次地循环:

class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        n = self.start
        while n > 0:
            yield n
            n -= 1

c = Countdown(3)
print(list(c), list(c))

# [3, 2, 1] [3, 2, 1]

何时使用发电机

使用发电机时 使用列表时
数据量较大或大小未知 数据很小并且适合内存
您按顺序处理一次项目 你循环几次
你可以提前停下来 你需要len(),索引或切片
您正在读取流或文件 您需要对数据进行排序或反转
您正在构建管道 简单比记忆更重要

总体而言,生成器并不总是更快。如果您最终使用每个值,则总工作是相似的,并且对于小数据,列表理解甚至可以更快,因为它的每项开销更少。主要好处是内存使用,以及更快地获得第一个结果。

常见错误

重复利用耗尽的发电机

几乎每个人都会在某个时刻遇到这种情况。生成器会记住它已完成:

g = count_up(2)
print(list(g), list(g))

# [1, 2] []

第二次调用得到一个空列表,没有任何错误。再次调用该函数以获取新的生成器,或使用上面所示的类方法。

期望代码立即运行

def noisy():
    print("body runs")
    yield 1

n = noisy()        # nothing is printed
print(next(n))     # body runs, then 1

生成器顶部的参数检查直到第一个 next() 才会运行,因此错误可能会出现在远离导致错误的调用处。

其他陷阱

  • 调用len()或索引。生成器都不支持,并提出TypeError。
  • 过早关闭文件。 从 with 块内部返回生成器可能意味着文件在读取任何行之前关闭。将 yield 放入 with 块内。
  • 将所有内容包装在 list(). 中,这会消除内存优势。
  • 对微小数据使用一个。简单的列表通常更清晰。

最后的话

生成器是一个可以暂停的函数。这一想法为您提供了低内存使用、无尽的序列和干净的管道。为了将其付诸实践,请在代码中找到一个位置,该位置构建一个大列表,仅对其进行一次循环,然后将其转换为生成器。然后尝试在一个真实的日志文件上将两个小型生成器链接在一起。

常见问题

Python中的yield有什么作用?

Yield 产生一个值并暂停函数,保留其局部变量。当请求下一个值时,该函数从同一点恢复。

生成器和列表有什么区别?

列表将其所有值一次性存储在内存中。生成器按需计算每个值,使用很少的内存,并且通常只能消耗一次。

迭代器和生成器有什么区别?

每个生成器都是一个迭代器。生成器是最简单的编写方法,因为 Python 为您创建了 __iter__ 和 __next__ 方法。

你能循环生成器两次吗?

不会。一旦耗尽,它就不再产生任何结果,也不会引发错误。通过再次调用该函数来创建新的生成器,或将结果存储在列表中。

生成器比列表快吗?

并非总是如此。它们使用的内存少得多,并且可以更快地提供第一个结果,但是当您完全消耗少量数据时,列表理解可能会稍微快一些。

继续学习

与Stack Quora合作

需要测试、WordPress 或 SEO 方面的帮助?

我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。

继续阅读

更多指南 测试、人工智能和现代开发

喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。

人工智能

人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它

人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。

阅读文章 →
DevOps

环境变量解释:.env 文件、秘密和配置正确完成

使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。

阅读文章 →
软件开发

API 速率限制解释:算法、标头和处理 429 错误

API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。

阅读文章 →
测试自动化

Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误

使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。

阅读文章 →
软件测试

代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义

用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker Volumes 与 Bind Mounts:以正确的方式保留容器数据

Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。

阅读文章 →
测试自动化

TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式

TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。

阅读文章 →
软件测试

用户验收测试 (UAT):流程、清单和真实示例

用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。

阅读文章 →
数据库与 SQL

SQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱

通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。

阅读文章 →
数据库与 SQL

SQL CASE WHEN 语句:语法、实例和常见错误

SQL CASE WHEN 解释了我们运行的查询:简单和搜索表单、条件顺序、SUM 透视、COUNT 技巧、自定义排序、更新和要避免的 NULL 陷阱。

阅读文章 →
软件开发

什么是智威汤逊?通过工作示例解释 JSON Web 令牌

什么是智威汤逊? JSON Web Tokens 解释:这三个部分,在我们运行的代码中构建和篡改的令牌,JWT 与会话,存储,刷新令牌和安全错误。

阅读文章 →
软件开发

JavaScript 事件循环解释:调用堆栈、微任务和计时器以及示例

JavaScript 事件循环用我们运行的代码进行了解释:调用堆栈、任务和微任务队列、为什么 setTimeout 0 不是立即执行、阻塞、饥饿和面试测验。

阅读文章 →
软件开发

Python 生成器和 Yield 解释:通过真实示例节省内存

Python 生成器和yield 用我们运行的代码进行了解释:它们如何暂停和恢复、测量内存节省、管道、无限序列、yield from、发送和常见陷阱。

阅读文章 →
人工智能

什么是嵌入?向量和余弦相似度简单解释

什么是嵌入?关于向量、余弦相似度和语义搜索的简单语言指南,其中包含我们运行的一个小示例、常见用途、选择模型和已知限制。

阅读文章 →
DevOps

蓝绿部署与金丝雀部署:差异、权衡以及如何选择

蓝绿部署与金丝雀部署解释:每种部署的工作原理、成本、风险和回滚、我们测试的稳定用户分桶、数据库迁移、滚动更新以及如何选择。

阅读文章 →
软件开发

Git merge 与 rebase 的区别:示例及如何选择

用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。

阅读文章 →
软件开发

Python 列表推导式:语法、示例与何时避免使用

Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →