Python函数之生成器原理详解

Python 生成器(Generator)

目录

  1. 什么是生成器
  2. 生成器语法
  3. 生成器函数
  4. 生成器表达式
  5. 生成器的工作原理
  6. 使用方法
  7. 通过next()函数
  8. 通过for循环
  9. 其他方法
  10. 应用场景
  11. 生成器表达式与列表推导式的区别
  12. 常见问题与陷阱

什么是生成器

生成器(Generator)是Python中的一种特殊类型的迭代器(Iterator),它能够按需生成值,而不是一次性生成所有值。与列表、元组等容器类型的可迭代对象不同,生成器不会将所有数据存储在内存中,而是在每次请求时"即时"生成数据。

生成器的关键特点:

  • 是特殊的迭代器,遵循迭代器协议
  • 使用yield语句生成值并暂停执行
  • 函数状态会被保存,下次调用时从暂停处继续
  • 每次调用只提供一个数据
  • 不能"回头"访问已生成的值
  • 支持next()函数调用
  • 不支持下标索引访问
  • 惰性计算,按需生成数据
  • 生成器语法

    Python中创建生成器有两种方式:通过生成器函数或生成器表达式。

    生成器函数

    生成器函数是包含yield关键字的常规函数。当调用生成器函数时,它返回一个生成器对象,而不执行函数体。

    def simple_generator():
        """一个简单的生成器函数"""
        print("开始执行")
        yield 1
        print("第一次yield之后")
        yield 2
        print("第二次yield之后")
        yield 3
        print("结束")
    
    # 创建生成器对象
    gen = simple_generator()
    # 此时函数体尚未执行
    

    生成器表达式

    生成器表达式是创建生成器的简洁语法,类似于列表推导式,但使用圆括号而不是方括号。

    # 生成器表达式
    gen_expr = (x ** 2 for x in range(5))
    

    生成器的工作原理

    生成器的核心机制是函数执行的暂停与恢复:

    1. 当调用生成器函数时,返回一个生成器对象,但函数体不执行
    2. 当第一次调用next()方法时,函数开始执行,直到遇到yield语句
    3. yield语句返回一个值,并且函数执行暂停,保存所有当前的状态(局部变量等)
    4. 下一次调用next()方法时,函数从上次暂停的地方继续执行
    5. 当函数执行完毕时,生成器抛出StopIteration异常

    这种机制使得生成器能够"记住"它的状态,并在每次需要时生成下一个值。

    使用方法

    通过next()函数

    可以使用内置的next()函数来获取生成器的下一个值:

    def count_up_to(max):
        count = 1
        while count <= max:
            yield count
            count += 1
    
    counter = count_up_to(3)
    print(next(counter))  # 输出: 1
    print(next(counter))  # 输出: 2
    print(next(counter))  # 输出: 3
    
    try:
        print(next(counter))  # 抛出StopIteration异常
    except StopIteration:
        print("生成器已耗尽")
    

    通过for循环

    更常见的是使用for循环遍历生成器的值:

    # 使用for循环遍历生成器
    for num in count_up_to(5):
        print(num)
    # 输出: 1 2 3 4 5
    

    其他方法

    生成器对象还有其他高级方法:

    1. send() – 向生成器内部发送值
    def echo_generator():
        value = yield "准备接收"
        print(f"收到: {value}")
        while True:
            value = yield f"你发送了: {value}"
            print(f"收到: {value}")
    
    gen = echo_generator()
    print(next(gen))      # 初始化生成器,输出: 准备接收
    print(gen.send("你好"))  # 发送值并获取下一个值,输出: 你发送了: 你好
    
    1. throw() – 向生成器内部抛出异常
    def generator_with_exception():
        try:
            yield 1
            yield 2
        except ValueError:
            yield 'Got ValueError!'
    
    g = generator_with_exception()
    print(next(g))        # 输出: 1
    print(g.throw(ValueError))  # 输出: Got ValueError!
    
    1. close() – 关闭生成器
    def closable_generator():
        try:
            yield 1
            yield 2
            yield 3
        finally:
            print("生成器被关闭")
    
    g = closable_generator()
    print(next(g))  # 输出: 1
    g.close()       # 关闭生成器,输出: 生成器被关闭
    

    应用场景

    1. 处理大文件

    生成器特别适合处理大文件,因为它们不需要一次性将整个文件加载到内存中:

    def read_large_file(file_path):
        with open(file_path, 'r') as file:
            for line in file:
                yield line.strip()
    
    # 逐行处理大文件
    for line in read_large_file('huge_file.txt'):
        # 处理每一行...
        pass
    

    2. 生成无限序列

    生成器可以轻松创建无限序列,只在需要时计算值:

    def fibonacci():
        a, b = 0, 1
        while True:
            yield a
            a, b = b, a + b
    
    # 获取斐波那契数列的前10个数
    fib_gen = fibonacci()
    fibs = [next(fib_gen) for _ in range(10)]
    print(fibs)  # 输出: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
    

    3. 数据流处理和管道

    生成器可用于创建数据处理管道,每个生成器负责一个处理步骤:

    def read_data(file_path):
        with open(file_path, 'r') as file:
            for line in file:
                yield line.strip()
    
    def parse_data(lines):
        for line in lines:
            # 解析每行数据
            yield line.split(',')
    
    def filter_data(parsed_lines):
        for items in parsed_lines:
            if len(items) >= 3 and items[2].isdigit():
                yield items
    
    # 构建数据处理管道
    lines = read_data('data.csv')
    parsed = parse_data(lines)
    filtered = filter_data(parsed)
    
    for item in filtered:
        print(item)
    

    4. 提高内存效率

    对比处理100万个数的平方的两种方式:

    # 使用列表 - 占用大量内存
    squares_list = [i ** 2 for i in range(1000000)]
    sum_squares = sum(squares_list)
    
    # 使用生成器 - 内存高效
    sum_squares = sum(i ** 2 for i in range(1000000))
    

    生成器表达式与列表推导式的区别

    特性 生成器表达式 列表推导式
    语法 使用圆括号 (x for x in range(10)) 使用方括号 [x for x in range(10)]
    内存使用 惰性计算,内存高效 一次性创建所有元素,占用更多内存
    计算时机 按需计算 立即计算所有值
    访问方式 只能顺序访问,不能索引 可以通过索引随机访问
    重复使用 只能遍历一次 可以多次遍历
    速度 生成值时可能稍慢,但初始化快 初始化慢,但访问值更快

    示例:

    # 列表推导式
    list_comp = [x ** 2 for x in range(5)]
    print(list_comp)        # 输出: [0, 1, 4, 9, 16]
    print(list_comp[2])     # 输出: 4
    print(sum(list_comp))   # 输出: 30
    print(sum(list_comp))   # 还可以再次使用,输出: 30
    
    # 生成器表达式
    gen_expr = (x ** 2 for x in range(5))
    # print(gen_expr[2])    # 错误:生成器不支持索引访问
    print(sum(gen_expr))    # 输出: 30
    print(sum(gen_expr))    # 输出: 0 (生成器已耗尽,不能重用)
    

    常见问题与陷阱

    1. 生成器耗尽后不能重新使用

    一旦生成器被完全迭代,它就不能被重置或重新使用:

    def count_to_three():
        yield 1
        yield 2
        yield 3
    
    gen = count_to_three()
    print(list(gen))  # 输出: [1, 2, 3]
    print(list(gen))  # 输出: [] (生成器已耗尽)
    

    2. 在生成器上使用列表函数会消耗生成器

    将生成器转换为列表会消耗生成器:

    gen = (x for x in range(5))
    data = list(gen)  # 将生成器转换为列表
    print(data)       # 输出: [0, 1, 2, 3, 4]
    print(list(gen))  # 输出: [] (生成器已被消耗)
    

    3. 生成器的延迟绑定

    在生成器中使用循环变量时要小心延迟绑定:

    # 错误的做法
    funcs = [lambda: i for i in range(5)]
    results = [f() for f in funcs]
    print(results)  # 输出: [4, 4, 4, 4, 4]
    
    # 正确的做法
    funcs = [(lambda j=i: j) for i in range(5)]
    results = [f() for f in funcs]
    print(results)  # 输出: [0, 1, 2, 3, 4]
    

    总结

  • 生成器是一种特殊的迭代器,能够按需生成值,节省内存
  • 使用yield关键字创建生成器函数,或使用生成器表达式
  • 生成器在每次调用时暂停执行并保存状态
  • 生成器特别适合处理大型数据集、无限序列和数据流
  • 与列表不同,生成器只能前进不能后退,且不支持索引访问
  • 生成器表达式比列表推导式更节省内存,但只能遍历一次
  • 使用生成器能显著提高程序的内存效率和可扩展性
  • 作者:想知道哇

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python函数之生成器原理详解

    发表回复