python使用迭代生成器yield減少記憶體佔用的方法

嗨学编程發表於2024-04-28

在python編碼中for迴圈處理任務時,會將所有的待遍歷參量載入到記憶體中。

其實這本沒有必要,因為這些參量很有可能是一次性使用的,甚至很多場景下這些參量是不需要同時儲存在記憶體中的,這時候就會用到本文所介紹的迭代生成器yield。

1.基本使用

首先我們用一個例子來演示一下迭代生成器yield的基本使用方法,這個例子的作用是構造一個函式用於生成一個平方陣列02,12,22...。

在普通的場景中我們一般會直接構造一個空的列表,然後將每一個計算結果填充到列表中,最後return列表即可,對應的是這裡的函式square_number

而另外一個函式square_number_yield則是為了演示yield而構造的函式,其使用語法跟return是一樣的,不同的是每次只會返回一個值:

def square_number(length):
    s = []
    for i in range(length):
        s.append(i ** 2)
    return s
 
def square_number_yield(length):
    for i in range(length):
        yield i ** 2
 
if __name__ == '__main__':
    length = 10
    sn1 = square_number(length)
    sn2 = square_number_yield(length)
    for i in range(length):
        print (sn1[i], '\t', end='')

        print (next(sn2))

在main函式中我們對比了兩種方法執行的結果,列印在同一行上面,用end=''指令可以替代行末的換行符號,具體執行的結果如下所示:

[dechin@dechin-manjaro yield]$ python3 test_yield.py 
0       0
1       1
4       4
9       9
16      16
25      25
36      36
49      49
64      64

81      81

可以看到兩種方法列印出來的結果是一樣的。也許有些場景下就是需要持久化的儲存函式中返回的結果,這一點用yield也是可以實現的,可以參考如下示例:

def square_number(length):
    s = []
    for i in range(length):
        s.append(i ** 2)
    return s
 
def square_number_yield(length):
    for i in range(length):
        yield i ** 2
 
if __name__ == '__main__':
    length = 10
    sn1 = square_number(length)
    sn2 = square_number_yield(length)
    sn3 = list(square_number_yield(length))
    for i in range(length):
        print (sn1[i], '\t', end='')
        print (next(sn2), '\t', end='')

        print (sn3[i])

這裡使用的方法是直接將yield生成的物件轉化成list格式,或者用sn3 = [i for i in square_number_yield(length)]這種寫法也是可以的,在效能上應該差異不大。上述程式碼的執行結果如下:

[dechin@dechin-manjaro yield]$ python3 test_yield.py 
0       0       0
1       1       1
4       4       4
9       9       9
16      16      16
25      25      25
36      36      36
49      49      49
64      64      64
81      81      81

2.進階測試

在前面的章節中我們提到,使用yield可以節省程式的記憶體佔用,這裡我們來測試一個100000大小的隨機陣列的平方和計算。如果使用正常的邏輯,那麼寫出來的程式就是如下所示:

import tracemalloc
import time
import numpy as np
tracemalloc.start()
 
start_time = time.time()
ss_list = np.random.randn(100000)
s = 0
for ss in ss_list:
    s += ss ** 2
end_time = time.time()
print ('Time cost is: {}s'.format(end_time - start_time))
 
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
 
for stat in top_stats[:5]:

    print (stat)

這個程式一方面透過time來測試執行的時間,另一方面利用tracemalloc追蹤程式的記憶體變化。

這裡是先用np.random.randn()直接產生了100000個隨機數的陣列用於計算,那麼自然在計算的過程中需要儲存這些生成的隨機數,就會佔用這麼多的記憶體空間。

如果使用yield的方法,每次只產生一個用於計算的隨機數,並且按照上一個章節中的用法,這個迭代生成的隨機數也是可以轉化為一個完整的list的:

import tracemalloc
import time
import numpy as np
tracemalloc.start()
 
start_time = time.time()
def ss_list(length):
    for i in range(length):
        yield np.random.random()
 
s = 0
ss = ss_list(100000)
for i in range(100000):
    s += next(ss) ** 2
end_time = time.time()
print ('Time cost is: {}s'.format(end_time - start_time))
 
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
 
for stat in top_stats[:5]:

    print (stat)

這兩個示例的執行結果如下,可以放在一起進行對比:

[dechin@dechin-manjaro yield]$ python3 square_sum.py 
Time cost is: 0.24723434448242188s
square_sum.py:9: size=781 KiB, count=2, average=391 KiB
square_sum.py:12: size=24 B, count=1, average=24 B
square_sum.py:11: size=24 B, count=1, average=24 B
[dechin@dechin-manjaro yield]$ python3 yield_square_sum.py 
Time cost is: 0.23023390769958496s
yield_square_sum.py:9: size=136 B, count=1, average=136 B
yield_square_sum.py:14: size=112 B, count=1, average=112 B
yield_square_sum.py:11: size=79 B, count=2, average=40 B
yield_square_sum.py:10: size=76 B, count=2, average=38 B

yield_square_sum.py:15: size=28 B, count=1, average=28 B

經過比較我們發現,兩種方法的計算時間是幾乎差不多的,但是在記憶體佔用上yield有著明顯的優勢。當然,也許這個例子並不是非常的恰當,但是本文主要還是介紹yield的使用方法及其應用場景。

3.無限長迭代器

在參考連結1中提到了一種用法是無限長的迭代器,比如按順序返回所有的素數,那麼此時我們如果用return來返回所有的元素並儲存到一個列表裡面,就是一個非常不經濟的辦法,所以可以使用yield來迭代生成,參考連結1中的原始碼如下所示:

def get_primes(number):
    while True:
        if is_prime(number):
            yield number

        number += 1

那麼類似的,這裡我們用while True可以展示一個簡單的案例——返回所有的偶數:

def yield_range2(i):
    while True:
        yield i
        i += 2
        
#學習中遇到問題沒人解答?小編建立了一個Python學習交流群:153708845 
iter = yield_range2(0)
for i in range(10):

    print (next(iter))

因為這裡我們限制了長度是10,所以最終會返回10個偶數:

[dechin@dechin-manjaro yield]$ python3 yield_iter.py 
0
2
4
6
8
10
12
14
16

18

總結

本文介紹了python的迭代器yield,其實關於yield,我們可以簡單的將其理解為單個元素的return。

這樣不僅就初步理解了yield的使用語法,也能夠大概瞭解到yield的優勢,也就是在計算過程中每次只佔用一個元素的記憶體,而不需要一直儲存大量的元素在記憶體中。

相關文章