提升性能与效率:利用生成器优化你的Python代码

176 阅读5分钟

前言

这节内容主要介绍生成器、如何创建生成器、生成器与普通函数的区别、生成器推导、生成器与迭代器的区别、生成器在实战中的应用

生成器

我们知道,函数体包含 yield 关键字的函数不是一个普通函数。这种函数叫做 生成器 ( generator )。

创建生成器

生成器创建和函数一样。我们举个例子

示例1

还记得在迭代器那片文章中,我们使用迭代器实现斐波那契数列,这里我们使用生成器实现

def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a+b
​
f = fib()
for i in range(10):
    print(next(f))
​
'''
0
1
1
2
3
5
8
13
21
34
'''

这个 fib 函数是一个生成器函数,它使用两个变量 ab 来保存当前斐波那契数列中的两个相邻数字。在每次迭代中,它使用 yield 关键字产生当前数字 a,然后更新 ab 的值,以继续生成下一个数字。这段代码将生成前 10 个斐波那契数列的数字并打印出来。

生成器与普通函数区别

生成器不是使用return返回一个值,而是可以生成多个值,每次一个。每次使用yield生成一个值后,函数都将冻结,即在此停止执行,等待被重新唤醒。被重新唤醒后,函数将从停止的地方开始继续执行。

注意:有人要问了,是如何被重新唤醒的呢?就是next()函数,next 返回本次结果,并暂停在 yield。

生成器推导

看这段代码,你一定熟悉

l = [i**2 for i in range(10)] # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

这段代码生成前10个数字的平方,也就是列表推导式。

那什么是生成器推导呢?其工作原理与列表推导相同,但不是创建一个列表(即不立即执行循环),而是返回一个生成器,让你能够逐步执行计算。像下面这样

l = (i**2 for i in range(10))
print(l) # <generator object <genexpr> at 0x1099065f0>
print(next(l)) # 0

相信你已经看到了区别,不同于列表推导,这里使用的是圆括号。

递归式生成器

递归式生成器是基于递归算法的生成器。它通过在生成器函数内部调用自身来生成序列中的下一个元素。递归式生成器在生成每个元素时依赖前一个元素,从而实现递归的效果。

示例1

使用递归式生成器实现斐波那契数列

def fibonacci_recursive(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)
​
def fibonacci_generator_recursive():
    n = 0
    while True:
        yield fibonacci_recursive(n)
        n += 1

在这个例子中,fibonacci_recursive 函数是一个递归函数,用于计算斐波那契数列中第 n 个数字。fibonacci_generator_recursive 函数是一个递归式生成器函数,通过调用 fibonacci_recursive 来生成斐波那契数列。

你可以使用以下代码来打印前 10 个斐波那契数列的数字:

fib = fibonacci_generator_recursive()
for i in range(10):
    print(next(fib))
​

这样就可以使用递归式生成器生成斐波那契数列的序列。

实战

实战一

文件处理:当需要处理大型文件时,逐行读取并一次性加载整个文件到内存可能会导致内存溢出。这时可以使用生成器来逐行读取文件,每次只加载一行到内存中,并通过生成器返回给调用者。这样可以有效地减少内存占用,并且在处理大型文件时提高性能。

def read_file_line_by_line(file_path):
    with open(file_path, 'r') as file:
        for line in file:
            yield line.strip()
​
# 使用生成器逐行读取文件并处理每一行
file_generator = read_file_line_by_line('example.txt')
for line in file_generator:
    # 在这里对每一行进行处理
    print(line)
​

实战二

数据流处理:在数据流处理的场景中,数据往往是连续不断地产生或传输的。生成器可以用于处理这些连续的数据流,通过生成器函数逐个生成数据,并在需要的时候进行处理。这样可以避免一次性加载所有数据,而是按需处理和生成,提高处理效率

def process_data_stream(data_stream):
    for data in data_stream:
        # 在这里对数据进行处理
        processed_data = process(data)
        yield processed_data
​
# 模拟数据流,例如从网络接收实时数据
data_stream = receive_data_stream()
processed_data_generator = process_data_stream(data_stream)
for data in processed_data_generator:
    # 在这里处理已处理的数据
    print(data)
​

实战三

实时日志分析:在实时日志分析的应用中,日志通常是不断产生的,需要对日志进行实时处理和分析。生成器可以用于读取日志文件或接收实时日志流,并逐行生成日志数据记录。通过生成器可以实现高效的实时日志分析,同时节省内存消耗。

def analyze_logs(log_file_path):
    with open(log_file_path, 'r') as log_file:
        for log_line in log_file:
            log_data = parse_log(log_line)
            yield log_data
​
# 对实时日志进行分析
log_generator = analyze_logs('logs.txt')
for log in log_generator:
    # 在这里处理每条分析后的日志
    print(log)
​

生成器与迭代器区别

1、实现方式:生成器是通过函数来创建的,使用关键字 yield 来产生一个值,并在需要时暂停执行。而迭代器是通过类来创建的,实现了 __iter__()__next__() 方法,用于返回下一个元素。

2、内存占用:生成器只在需要时才会生成和返回一个元素,因此在内存上只存储当前生成的元素以及生成器的状态信息。而迭代器可能需要在内存中保留整个数据集,尤其是当使用列表等容器类型作为迭代器时。

3、数据生成方式:生成器可以通过循环、递归等方式动态生成数据,因此对于无限序列或者大型数据集非常适用。而迭代器通常是基于已经存在的数据集合进行迭代。

最后

到这里生成器应该了解差不多了,最后我们在说一下生成器是否可以遍历多次呢?

对于一个有限元素的生成器,只可以遍历一次,看下面这个例子

def number_generator():
    num = 1
    while num<10:
        yield num
        num += 1
​
generator = number_generator()
​
print(list(generator))  # 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list(generator))  # 输出: []

可以看到,再次输出时,为空列表,如果是使用next()返回当前结果,生成器消耗完后,next会报StopIteration的错误。