Post

【Python 高级特性】生成器:处理大量数据时,节省内存和时间

Hi everyone, Welcome to my channel.

在这篇文章中,我会向你介绍生成器,一个在处理大量数据时很有用的工具。

如果你曾在列表中放入大量数据,你会发现程序跑得很慢。这是因为列表会将所有的值都存在内存中,导致大量内存被占用,影响程序运行效率。

使用生成器就不会出现这样的问题,因为生成器不会一次存储所有值,而是只在每次需要时才生成一个值,让程序能够高效运行。

通过这篇文章,你会知道如何创建和使用生成器,并看到生成器相对于列表在内存和时间上的巨大优势。

定义生成器的两种方式

定义生成器有两种方法,生成器函数和生成器表达式。我想先展示生成器函数,因为这会让你更好理解生成器是如何运作的。

生成器函数

让我们看看下面的函数,它的功能很简单。

1
2
3
4
5
6
7
8
def square_numbers(nums):
    result = []
    for i in nums:
        result.append(i * i)
    return result

my_nums = square_numbers([1,2,3,4,5])
print(my_nums)

函数 square_numbers 接受一个列表,通过循环,将列表中每个元素平方,添加到一个新列表中,最后返回新列表。以上代码会打印出 [1, 4, 9, 16, 25]

现在我要将这个函数改写为生成器。

1
2
3
4
5
6
def square_numbers(nums):
    for i in nums:
        yield i * i

my_nums = square_numbers([1,2,3,4,5])
print(my_nums)

去掉新定义的列表,去掉 return ,将 append 改为 yield

这个地方的知识点是,如果一个函数包含 yield 语句,那么它是一个生成器。每当调用这个函数时,就会从 yield 处返回一个生成的值。再次执行时,从上次返回的 yield 处继续。

在这个例子中,第一次运行会先以 i 等于 1 运行到 yield,生成一个 1 的平方。然后第二次运行时,从 yield 处继续,以 i 等于 2 运行到 yield,生成 2 的平方。

运行上面的代码,打印出来的结果类似于 <generator object square_numbers at 0x7f9e3c3e3d60>,不像列表可以直接看到值。这是因为,生成器不会将所有值都存储在内存中,只在需要的时候才生成一个值。

如果要查看具体的值,我们可以使用 next 函数,也可以使用 for 进行遍历。下面的例子先使用了 next 然后使用 for ,看看会发生什么。

1
2
3
4
5
6
print('Running next function:')
print(next(my_nums))

print('Then running for loop:')
for num in my_nums:
    print(num)

以上代码会打印出:

1
2
3
4
5
6
7
Running next function:
1
Then running for loop:
4
9
16
25

稍微有点奇怪,使用 next 函数返回第一个值 1 后,再从 for 循环中返回的是从第二个值开始的。这是因为生成器每次只会生成一个词,并记住上一次的位置。所以,当生成第一个值后,再生成的就是第二个值了。

生成器表达式

现在,来看看生成器的另一种写法,生成器表达式。

我们上次介绍过了列表推导式,它可以非常方便地创建一个新列表。如果要实现刚刚的列表平方功能,我们可以这样写:

1
2
my_list = [i * i for i in [1,2,3,4,5]]
print(my_list)

生成器也存在着这样的方法,我们只需要将列表推导式中的方括号 [] 替换为圆括号 () 即可。就像这样:

1
my_gen = (i * i for i in [1,2,3,4,5])

生成器的优势

生成器相对于列表的优势在于,它不会一次性生成所有的值,而是在需要的时候才生成。想象你需要处理一个非常大的数据集,如果使用列表,它会一次性生成所有的值,占用大量内存。而生成器则不会,它只会在需要的时候生成一个值,然后释放内存。

让我们来看一个例子,在这个例子中,我会分别使用列表和生成器来生成 1000000 个随机数,看看列表和生成器分别会占用多少时间和内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import psutil, time, os, random

random_nums = [i for i in range(10)]

list_start_time, list_start_mem = time.time(), psutil.Process(os.getpid()).memory_info().rss/1024/1024
num_list = [random.choice(random_nums) for _ in range(1000000)]
list_end_time, list_end_mem = time.time(), psutil.Process(os.getpid()).memory_info().rss/1024/1024
print(f'''Before building list, program takes memory: {list_start_mem} MB
      Took time: {list_end_time - list_start_time} seconds
      After building list, program takes memory: {list_end_mem} MB''')

gen_start_time, gen_start_mem = time.time(), psutil.Process(os.getpid()).memory_info().rss/1024/1024
num_gen = (random.choice(random_nums) for _ in range(1000000))
gen_end_time, gen_end_mem = time.time(), psutil.Process(os.getpid()).memory_info().rss/1024/1024
print(f'''Before building generator, program takes memory: {gen_start_mem} MB
      Took time: {gen_end_time - gen_start_time} seconds
      After building generator, program takes memory: {gen_end_mem} MB''')

这段代码在不同的机器上运行会有不同的结果,在我的电脑上运行这个代码得到的结果是:

1
2
3
4
5
6
7
Before building list, program takes memory: 28.41796875 MB
Took time: 3.9176957607269287 seconds
After building list, program takes memory: 36.4765625 MB
Before building generator, program takes memory: 36.4765625 MB
Took time: 2.6226043701171875e-06 seconds
After building generator, program takes memory: 36.4765625 MB

看,生成器在生成 1000000 个随机数时,所占用的内存几乎没有变化,而列表则占用了 8 MB 的内存。同时,生成器的生成时间也比列表快了许多。

所以,在处理大量数据时,生成器是一个非常好的选择。

最后

通过这篇文章,现在的你应该能够了解生成器的优势,并在适当的时候创建属于你的生成器了。你可以检查你的历史代码,看看有没有办法进行优化。

如果你有任何问题,欢迎在评论区留言,我会尽快回复。 也可以关注我的频道,以避免错过更多 Python 相关的内容。感谢你的阅读。

This post is licensed under CC BY 4.0 by the author.