【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 相关的内容。感谢你的阅读。