Python函数怎样写一个生成器函数实现迭代 Python函数生成器创建与使用的简单教程​

爱谁谁
发布: 2025-08-15 15:11:01
原创
739人浏览过

生成器函数的核心是使用yield关键字,它使函数在每次遇到yield时暂停并返回值,保持状态以便后续恢复;2. 与普通函数一次性返回所有结果不同,生成器采用惰性计算,按需生成数据,显著降低内存占用;3. 生成器对象只能迭代一次,耗尽后需重新创建;4. 常见应用场景包括处理大文件、构建数据流管道和实现无限序列;5. 性能上生成器内存效率高,但小数据集可能因上下文开销略慢于列表;6. 使用yield from可优雅地委托子生成器,提升代码简洁性和健壮性。

Python函数怎样写一个生成器函数实现迭代 Python函数生成器创建与使用的简单教程​

Python中,要写一个生成器函数来实现迭代,核心在于使用

yield
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
关键字。它不像普通函数那样用
return
登录后复制
一次性返回所有结果,而是每次遇到
yield
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
就“暂停”执行,并返回一个值,然后记住当前执行状态,等待下一次被请求时再从上次暂停的地方继续。这样,我们就能得到一个可迭代的对象,但它并不会一次性生成所有数据并存储在内存里,而是按需生成,这对于处理大量数据或无限序列非常有用。

解决方案

编写一个生成器函数非常直观,你只需要在函数体内部使用

yield
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
语句来产生序列中的每个元素。当你调用这个函数时,它不会立即执行函数体内的代码,而是返回一个生成器对象(一个迭代器)。只有当你开始迭代这个生成器对象(比如通过
for
登录后复制
登录后复制
登录后复制
登录后复制
循环,或者手动调用
next()
登录后复制
登录后复制
登录后复制
登录后复制
)时,函数体内的代码才会开始执行,直到遇到第一个
yield
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
语句,返回一个值,然后暂停。下一次迭代请求时,它会从上次暂停的地方继续执行,直到遇到下一个
yield
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
,或者函数执行完毕。

def simple_generator():
    print("开始生成...")
    yield 1
    print("生成了1,继续...")
    yield 2
    print("生成了2,快结束了...")
    yield 3
    print("生成完毕!")

# 创建生成器对象
gen = simple_generator()

# 迭代生成器
print("--- 第一次迭代 ---")
for value in gen:
    print(f"获取到值: {value}")

# 尝试再次迭代同一个生成器(会发现它已经耗尽)
print("\n--- 尝试第二次迭代同一个生成器 ---")
for value in gen:
    print(f"再次获取到值: {value}") # 这行不会被执行,因为生成器已经空了

# 如果想再次迭代,需要重新创建一个生成器对象
print("\n--- 重新创建生成器并迭代 ---")
new_gen = simple_generator()
print(next(new_gen)) # 手动获取下一个值
print(next(new_gen))
print(next(new_gen))
try:
    print(next(new_gen)) # 尝试获取第四个值,会抛出StopIteration
except StopIteration:
    print("生成器已耗尽,无法再获取值。")
登录后复制

从上面的例子可以看出,生成器对象一旦被遍历完,就“空”了,不能再用来生成值。如果你需要再次遍历,就得重新调用生成器函数,创建一个新的生成器对象。这和列表这样的数据结构完全不同,列表可以反复遍历。

立即学习Python免费学习笔记(深入)”;

生成器函数与普通函数:它们在内存使用和性能上有何差异?

我个人觉得,理解生成器函数和普通函数的根本区别,是掌握其精髓的关键。普通函数在执行时,通常会一次性完成所有计算,并返回一个最终结果(比如一个列表、一个字典)。如果这个结果集很大,那么在函数返回之前,所有数据都得老老实实地待在内存里。这对于处理几十万、上百万甚至更多条记录的情况,简直是内存杀手。我以前就遇到过因为一次性加载一个巨大CSV文件到内存,导致程序直接崩溃的窘境。

而生成器函数则完全不同,它采用的是“惰性计算”或者说“按需生成”的策略。当你调用生成器函数时,它并不会立即执行函数体内的所有代码,而是返回一个生成器对象。这个对象就像一个“承诺”,它承诺在你需要的时候,会给你下一个值。只有当你明确地请求(比如在

for
登录后复制
登录后复制
登录后复制
登录后复制
循环中,或者调用
next()
登录后复制
登录后复制
登录后复制
登录后复制
)时,生成器函数才会执行一小段代码,生成一个值,然后暂停,释放掉当前不需要的资源。下一个值?等你需要的时候再说。

这种机制带来的最大好处就是内存效率。你不需要一次性把所有数据都加载到内存中,这对于处理大文件、无限序列(比如斐波那契数列,你可以生成到任意大,而不用担心内存溢出)或者实时数据流(比如日志分析、网络数据包处理)来说,简直是救星。它不是为了追求纯粹的CPU执行速度,而是为了在资源受限的环境下,更优雅、更高效地处理数据。当然,由于每次

yield
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
next
登录后复制
调用之间存在一些上下文切换的开销,对于非常小的数据集,生成器可能比直接构建列表略慢,但这种差异通常可以忽略不计,而且内存优势往往更重要。

在实际开发中,生成器函数有哪些常见的应用场景?

生成器函数在实际开发中的应用场景非常广泛,尤其是在需要处理大量数据、数据流或者实现自定义迭代逻辑时。

一个非常经典的例子就是读取大型文件。想象一下,你有一个几十GB的日志文件,如果试图用

readlines()
登录后复制
一次性读入内存,你的程序很可能直接崩溃。但如果用生成器:

def read_large_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as f:
        for line in f:
            yield line.strip() # 逐行返回,并去除空白字符

# 使用生成器处理文件
# 假设有一个很大的'access.log'文件
# for log_entry in read_large_file('access.log'):
#     # 处理每一行日志,例如解析、过滤等
#     if "ERROR" in log_entry:
#         print(f"发现错误日志: {log_entry}")
#     # 内存占用始终保持在较低水平
登录后复制

这样,每次

for
登录后复制
登录后复制
登录后复制
登录后复制
循环只处理一行,内存占用极低。

另一个我经常用的场景是数据流处理管道。你可以把一系列数据处理步骤串联起来,每一步都是一个生成器,前一个生成器的输出是后一个生成器的输入。例如,从文件读取数据,然后过滤,再转换:

def get_numbers(filepath):
    with open(filepath, 'r') as f:
        for line in f:
            try:
                yield int(line.strip())
            except ValueError:
                continue # 忽略非数字行

def filter_even(numbers_gen):
    for num in numbers_gen:
        if num % 2 == 0:
            yield num

def square_numbers(even_numbers_gen):
    for num in even_numbers_gen:
        yield num * num

# 假设'data.txt'包含多行数字和一些文本
# 例如:
# 1
# 2
# hello
# 3
# 4
# 5

# data.txt 内容:
# 1
# 2
# hello
# 3
# 4
# 5

# 运行管道
# for squared_even in square_numbers(filter_even(get_numbers('data.txt'))):
#     print(squared_even)
# 输出:
# 4
# 16
登录后复制

这种链式调用,每个环节都是惰性求值,数据在管道中流动,不会在任何一个中间步骤积累大量内存。

此外,自定义迭代器也是一个重要应用。当你需要实现一个自定义的复杂数据结构或者算法,并希望它能够像内置的列表、元组一样被

for
登录后复制
登录后复制
登录后复制
登录后复制
循环遍历时,生成器函数是实现
__iter__
登录后复制
方法的绝佳选择。比如,实现一个无限斐波那契数列生成器:

def fibonacci_sequence():
    a, b = 0, 1
    while True: # 无限序列
        yield a
        a, b = b, a + b

# for i, num in enumerate(fibonacci_sequence()):
#     if i > 10:
#         break
#     print(num)
# 输出:
# 0
# 1
# 1
# 2
# 3
# 5
# 8
# 13
# 21
# 34
# 55
登录后复制

你甚至可以利用生成器实现一些简单的协程行为,虽然现在有了

async/await
登录后复制
,但生成器的
send()
登录后复制
throw()
登录后复制
close()
登录后复制
方法依然为一些高级并发模式提供了基础。

编写生成器函数时,有哪些常见的陷阱或性能考量?

编写生成器函数时,有一些细节确实需要留意,否则可能会遇到一些意想不到的行为。

一个最常见的“陷阱”就是生成器只能被迭代一次。这跟列表、元组这种数据结构不一样。当你把一个生成器对象传给一个函数,那个函数遍历了一遍,那么这个生成器就“用完了”。如果你想再次遍历,或者把它传给另一个函数,那个函数会发现它已经空了。我第一次遇到这个问题时,还以为是代码哪里写错了,后来才明白这是生成器的设计特性。

def my_numbers():
    yield 1
    yield 2
    yield 3

gen_obj = my_numbers()
list_one = list(gen_obj) # 第一次迭代,gen_obj被耗尽
print(f"第一次转换成列表: {list_one}") # 输出: [1, 2, 3]

list_two = list(gen_obj) # 第二次尝试转换,gen_obj已经空了
print(f"第二次转换成列表: {list_two}") # 输出: []

# 解决办法:每次需要迭代时,重新调用生成器函数创建新的生成器对象
gen_obj_new = my_numbers()
list_three = list(gen_obj_new)
print(f"重新创建后转换成列表: {list_three}") # 输出: [1, 2, 3]
登录后复制

所以,如果你在一个地方使用了生成器,并且知道后面可能还需要它,要记得重新创建它,或者一开始就考虑是否真的需要一个生成器,还是一个列表更合适。

性能考量方面,虽然生成器在内存效率上表现出色,但它并非总是比列表推导式或一次性构建列表更快。每次

yield
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
登录后复制
next()
登录后复制
登录后复制
登录后复制
登录后复制
的调用,都会涉及一些内部的上下文切换和状态保存,这会带来微小的开销。对于非常小的数据集(比如只有几十个元素),这种开销可能会使得生成器在纯粹的CPU执行时间上略逊于直接构建列表。例如:

import time

def generate_small_list():
    return [i for i in range(1000)]

def generate_small_generator():
    for i in range(1000):
        yield i

start = time.perf_counter()
_ = generate_small_list()
end = time.perf_counter()
print(f"列表推导式耗时: {(end - start) * 1000:.2f} ms")

start = time.perf_counter()
_ = list(generate_small_generator()) # 强制迭代生成器
end = time.perf_counter()
print(f"生成器耗时: {(end - start) * 1000:.2f} ms")
登录后复制

你会发现,对于小规模数据,列表推导式可能更快。但请记住,生成器的核心优势是内存,而不是绝对速度。当数据量变得庞大时,内存优势会迅速抵消掉这点微小的速度劣势,并成为决定性的因素。

另一个需要注意的,是当你在生成器内部调用另一个生成器时,可以使用

yield from
登录后复制
登录后复制
。它能更优雅地处理子生成器的委托,避免了手动循环
next()
登录后复制
登录后复制
登录后复制
登录后复制
的繁琐。

def sub_generator():
    yield 'a'
    yield 'b'

def main_generator_manual():
    yield 1
    for item in sub_generator(): # 手动迭代子生成器
        yield item
    yield 2

def main_generator_yield_from():
    yield 1
    yield from sub_generator() # 使用yield from委托
    yield 2

print(list(main_generator_manual()))     # 输出: [1, 'a', 'b', 2]
print(list(main_generator_yield_from())) # 输出: [1, 'a', 'b', 2]
登录后复制

yield from
登录后复制
登录后复制
不仅代码更简洁,在处理异常和关闭子生成器时也更健壮。理解这些细节,能帮助我们更高效、更稳定地利用生成器。

以上就是Python函数怎样写一个生成器函数实现迭代 Python函数生成器创建与使用的简单教程​的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 //m.sbmmt.com/ All Rights Reserved | php.cn | 湘ICP备2023035733号