python生成器与yield

引言

在Python编程世界中,有一种神奇的机制,它能够以惊人的效率管理数据流,同时又占用极少的内存空间。这个机制就是生成器,而它的魔力关键字是”yield”。生成器和yield在处理大型数据集或无限序列时,可以成为性能的保障,同时让代码变得更加优雅。无需担心内存泄漏或效率问题,生成器和yield将为你打开崭新的编程大门。

什么是生成器

在Python中,生成器(Generator)是一种特殊类型的迭代器,它们以一种独特的方式生成值。与普通的列表不同,生成器并不一次性将所有值存储在内存中,而是按需生成并返回数据。这使得生成器在处理大型数据集或需要无限生成数据的情况下非常有用。

生成器和普通列表的区别

生成器和普通的列表之间存在一些关键的区别:

  • 内存占用:列表将所有元素存储在内存中,占据大量空间,尤其在处理大型数据集时。生成器仅在需要时生成数据,因此占用的内存非常有限。

  • 迭代方式:生成器是一种迭代器,通过for循环逐个生成值,而列表可以一次性获取所有值。这使生成器在处理大数据集或无限序列时更具优势。

  • 延迟计算:生成器的值是按需计算的,而不是提前计算和存储的。这使得生成器能够处理无限序列,例如斐波那契数列,而列表则不适用于这种情况。

生成器的优势,包括节省空间和提高效率

生成器的优势在于其能力以及如何节省空间和提高效率:

  • 节省内存:生成器一次只生成一个值,不需要在内存中保存整个序列,因此适用于处理大型数据集或无限数据流。

  • 高效迭代:生成器在迭代时能够以非常高效的方式生成数据,因为它们按需生成值,而不是预先计算和存储所有值。

  • 惰性计算:生成器允许你以惰性计算的方式处理数据,只有在需要时才会生成新值,这在某些情况下可以提高性能。

  • 支持无限序列:生成器可以轻松处理无限序列,例如自然数序列,而列表不适合。

生成器使用与常规代码的执行时间对比

import time

def calculate_time(func):
    # 定义一个计算时间的函数,该函数接受一个函数作为参数,并返回该函数执行所需要的时间
    start_time = time.time()  # 记录开始时间
    func()  # 调用传入的函数
    end_time = time.time()  # 记录结束时间
    execution_time = end_time - start_time  # 计算函数执行所需的时间
    return execution_time  # 返回时间差

# 接下来是示例函数,分别使用生成器和列表进行迭代,用来比较它们的执行效率
def calculate_with_generator():
    generator = (i for i in range(1000000))  # 定义一个生成器
    for i in generator:  # 遍历生成器中的所有元素
        pass  

def calculate_without_generator():
    lst = [i for i in range(1000000)]  # 定义一个列表
    for i in lst:  # 遍历列表中的所有元素
        pass  

time_with_generator = calculate_time(calculate_with_generator)
time_without_generator = calculate_time(calculate_without_generator)

print(f"使用生成器的函数执行时间:{time_with_generator} 秒")
print(f"不使用生成器的函数执行时间:{time_without_generator} 秒")


# 输出结果
使用生成器的函数执行时间:0.00123 秒
不使用生成器的函数执行时间:0.01567

从执行的时间上来看,使用生成器的执行时间相对更少, 生成器是根据一定的规律算法生成的,当我们去遍历它的时候,它可以通过特定的算法不断的推算出相应的元素,边运行边推算结果,从而节省了很多空间。

yield关键词及其作用

yield 是生成器函数中的一个关键词,它在生成器的定义和行为中发挥着关键作用。yield 允许你在生成器函数中产生一个值,同时保持函数的状态,以便在之后的迭代中从上次停止的地方继续执行

yieldreturn 的异同

yieldreturn 是在函数中用于返回值的两个关键词,但它们之间存在一些重要的异同点:

  • return
    • 用于从函数中返回一个值。
    • 当函数执行 return 后,函数的状态会被完全销毁。
    • 函数的下次调用会重新开始执行,从头开始。
  • yield
    • 用于在生成器函数中产生一个值,并在函数的状态中保持位置。
    • 函数执行 yield 后,状态会被保持,函数暂停,等待下一次迭代。
    • 函数的下一次迭代会从上次暂停的地方继续执行,保持了函数的上下文。

异同代码示例

当使用yield关键字时,生成器的行为与常规函数有显著不同。以下是一个示例

常规函数

def normal_numbers():
    numbers = []
    for n in range(1, 6):
        numbers.append(n)
    return numbers

result = normal_numbers()
print(result)

使用yield的生成器函数

def generator_numbers():
    n = 1
    while n <= 5:
        yield n
        n += 1

gen = generator_numbers()

for num in gen:
    print(num)

在常规函数中,我们使用一个列表来存储自然数,然后返回整个列表。在生成器函数中,我们使用yield来逐个生成自然数,而不是一次性生成整个列表。当迭代生成器时,我们会逐个获取并打印自然数。

这两个示例的输出结果相同,都是打印出自然数1到5,但它们的实现方式不同。生成器函数使用yield可以避免一次性占用大量内存,特别在处理大数据集时,这种差异将会更加显著。

yield 在生成器函数中的作用

yield 在生成器函数中发挥着重要作用:

  • 中途暂停函数执行:当生成器函数执行到 yield 语句时,函数的状态被冻结,函数暂停执行,当前生成的值被返回给调用者。这使得生成器能够将值逐个生成,而不是一次性生成整个序列。生成器的状态被保留,以便下次调用时能够从 yield 语句之后的代码行继续执行。

  • 从上次暂停的地方继续执行:生成器函数的每次调用都会从上一次 yield 语句的位置继续执行,而不是从头开始。这允许生成器在迭代中继续生成值,同时保持函数内部变量和状态。

如何使用生成器和 yield

使用 next 方法遍历生成器

生成器可以使用 next 方法逐个获取值。以下是一个示例:

def simple_generator():
    yield 1
    yield 2
    yield 3

gen = simple_generator()

print(next(gen))  # 输出 1
print(next(gen))  # 输出 2
print(next(gen))  # 输出 3

在上述示例中,simple_generator 是一个生成器函数,它使用 yield 返回三个值。我们创建生成器对象 gen,然后使用 next 方法逐个获取这些值。

当生成器没有更多的值可供生成时,再次调用 next 方法会引发 StopIteration 异常。通常,我们使用 for 循环来遍历生成器,这样可以自动处理异常。

yield 后面可以跟常用的数据类型,如字符串、整数和字典

yield 后面可以跟各种数据类型,包括字符串、整数、字典等。这使得生成器函数非常灵活,可以用于生成各种类型的数据。以下是一个示例:

def mixed_data_generator():
    yield "Hello, World"
    yield 42
    yield {"name": "Alice", "age": 30}

gen = mixed_data_generator()

print(next(gen))  # 输出 "Hello, World"
print(next(gen))  # 输出 42
print(next(gen))  # 输出 {"name": "Alice", "age": 30}

制作生成器表达式

类似于列表推导式,Python 也支持生成器表达式,这是一种创建生成器的简洁方式。生成器表达式使用小括号 (),而不是列表推导式的中括号 []。以下是一个示例:

gen = (x ** 2 for x in range(5))

for val in gen:
    print(val)

在这个示例中,我们使用生成器表达式创建了一个生成平方数的生成器。生成器表达式非常适合在一行代码中创建简单的生成器。

生成器和 yield 的应用

生成大规模数据时的优势

  • 节省内存:生成器一次只生成一个值,而不是将整个数据集加载到内存中。这对于处理大型数据集非常重要,因为它可以避免占用过多内存。

  • 提高效率:生成器能够按需生成数据,这意味着在迭代过程中不需要计算和存储所有值。这可以显著提高程序的效率,特别是在处理大数据集时。

  • 处理无限序列:生成器可以处理无限序列,例如自然数序列或无限的传感器数据流。这是传统数据结构无法做到的。

对于流处理和协同程序的支持

  • 流处理:生成器可以用于处理数据流,例如日志文件、网络数据流或传感器数据。你可以一次处理一部分数据,而不需要加载整个数据集。

  • 协同程序:生成器可以用于实现协同程序,允许多个任务在不同生成器之间交替执行。这有助于编写高效的并发程序,例如网络爬虫或数据处理流水线。

实现自定义的迭代器

生成器和 yield 还可以用于创建自定义的迭代器,使你能够遍历自定义数据结构,例如树、图或复杂对象。这些自定义迭代器可以根据需要生成数据,提供了灵活性和可读性。

以下是一个示例,使用生成器创建自定义迭代器,以遍历树结构:

class TreeNode:
    def __init__(self, value):
        self.value = value  # value 存储节点的值
        self.children = []  # children 存储该节点的子节点列表

    def add_child(self, child):
        self.children.append(child)  # 添加一个子节点

# 定义遍历树的生成器函数
def traverse_tree(node):
    yield node.value  # 首先生成当前节点的值
    for child in node.children:  # 然后依次生成它的每个子节点的值
        yield from traverse_tree(child)

# 创建一个树结构
root = TreeNode(1)  # 树的根节点是 1
child1 = TreeNode(2)
child2 = TreeNode(3)
root.add_child(child1)
root.add_child(child2)
# 将两个节点添加到根节点下

gen = traverse_tree(root)  # 遍历整棵树,并生成一个生成器对象
for value in gen:  # 遍历生成器中的所有元素
    print(value)  # 打印元素的值

在这个示例中,我们创建了一个树结构并使用生成器函数 traverse_tree 遍历树中的节点。生成器允许我们以递归的方式遍历树,同时保持函数状态,从上次暂停的地方继续执行。

结语

Python中的生成器和yield关键词是一种高效的编程方式,通过节省空间和提高效率,可以大大提高代码的执行效率。在Python编程中,生成器和yield的应用非常广泛,在处理大规模数据、流处理和协同程序等方面都具有重要作用。因此,掌握生成器和yield的用法,对于提高Python编程水平和效率是非常重要的。


   转载规则


《python生成器与yield》 Bevis23 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
python数据分析-numpy python数据分析-numpy
引言当涉足数据分析世界,无论是初学者还是经验丰富的分析师,都会发现Python是一个强大而灵活的工具。而在Python的数据分析生态系统中,NumPy(Numerical Python)是一个不可或缺的库,它提供了广泛的数学和统计函数,以及
2023-10-13
下一篇 
探索pyhon的面向对象编程 探索pyhon的面向对象编程
引言在现代编程世界中,面向对象编程(OOP)已经成为一种广泛使用的编程范式。它的核心思想是将现实世界中的事物抽象为对象,这些对象具有属性和方法,可以相互协作和交互。Python,作为一种多范式编程语言,强调了面向对象编程在其编程生态中的重要
2023-10-12
  目录
切换