連Python產生器(Generator)的原理都解釋不了,還敢說Python用了5年?

 

 

 

最近有很多學Python同學問我,Python Generator到底是什麼東西,如何理解和使用。Ok,現在就用這篇文章對Python Generator做一個敲骨瀝髓的深入解析。
 
為了更好地理解產生器(Generator),還需要掌握另外兩個東西:yield和迭代(iterables)。下面就迭代、產生器和yield分別做一個深入的解析。
 
1. 迭代
 
當創建一個列表對象後,可以一個接一個讀取列表中的值,這個過程就叫做迭代。
mylist = [1, 2, 3]
for i in mylist:
    print(i, end = ' ')
 
mylist對象是可迭代的。在創建列表時,可以使用列表推導表達式,所以從直觀上看,列表是可迭代的。
 
mylist = [x*x for x in range(3)]
for i in mylist:
    print(i, end=' '
 
只要使用for … in…語句,那麼in子句後面的部分一定是一個可迭代的對象,如列表、字典、字元串等。
 
這些可迭代的對象在使用上非常容易理解,我們可以用自己期望的方式讀取其中的值。但會帶來一個嚴重的問題。就拿列表為例,如果需要迭代的值非常多,這就意味著需要先將所有的值都放到列表中,而且即使迭代完了列表中所有的值,這些值也不會從記憶體中消失(至少還會存在一會)。而且如果這些值只需要迭代一次就不再使用,那麼這些值在記憶體中長期存在是沒有必要的,所有就產生了產生器(Generator)的概念。
 
2. 產生器(Generator)
 
要理解產生器,首先要清楚產生器到底要解決什麼問題,以及產生器的特性。
 
產生器只解決一個問題,就是讓需要迭代的值不再常駐記憶體,也就是解決的記憶體資源消耗的問題。
為了解決這個問題,產生器也要付出一定的代價,這個代價就是產生器中的值只能訪問一次,這也是產生器的特性。
 
下面先看一個最簡單的產生器的例子:
# 創建產生器
data_generator = (x*x for x in range(3))
print(data_generator)
for i in data_generator:
    print(i, end=' ')
print()
print('第二次迭代data_generator,什麼都不會輸出')
print()
for i in data_generator:
    print(i, end=' ')
 
乍一看這段程式碼,好像與前面的程式碼沒什麼區別。其實,只有一點點區別,就是在創建data_generator對象時使用了一對圓括弧,而不是一對方括弧。使用一對方括弧創建的是列表對象,而使用一對圓括弧創建的就是迭代器對象,如果直接輸出,會輸出迭代器對象的地址,只有通過for…in…語句或調用迭代器的相應方法才能輸出迭代器對象中的值。而且第二次對迭代器對象進行迭代,什麼都不會輸出,這是因為迭代器只能被迭代一次,而且被迭代的值使用完,是不會再保存在記憶體中的。有點類似熊瞎子摘苞米,摘一穗,丟一穗。
 

 

 

執行這段程式碼,會輸出如下結果:
 

<generator object <genexpr> at 0x7f95e0154150>
0 1 4 
第二次迭代data_generator,什麼都不會輸出
 
3. yield
 
到現在為止,我們已經對產生器要解決的問題,以及特性有了一個基本了解,那麼產生器是如何做到這一點的呢?這就要依靠yield語句了。
 
現在讓我們先來看一個使用yield的例子。
 

# 編寫產生器函數
def generate_even(max):
    for i in range(0, max + 1):
        if i % 2 == 0:
            yield i
print(generate_even(10))
even_generator = generate_even(10)
for n in even_generator:
    print(n, end=' ')
 
這段程式碼的目的是輸出不大於10的所有偶數,其中generator_even是一個產生器函數。我們注意到,在該函數中每找到一個偶數,就會通過yield語句指定這個偶數。那麼這個yield起什麼作用呢?
 
再看看後面的程式碼,首先調用generator_even函數,並將返回值賦給even_generator變數,這個變數的類型其實是一個產生器對象。而for…in…循環中的in子句後面則是這個產生器對象,而n則是產生器中的每一個值(偶數)。執行這段程式碼,會輸出如下結果:
 

<generator object generate_even at 0x7f814826c450>
0 2 4 6 8 10 
 
現在先談談執行yield語句會起到什麼效果。其實yield語句與return語句一樣,都起到返回的作用。但yield與return不同,如果執行return語句,會直接返回return後面表達式的值。但執行yield語句,返回的是一個產生器對象,而且這個產生器對象的當前值就是yield語句後面跟著的表達式的值。調用yield語句後,當前函數就會返回一個迭代器,而且函數會暫停執行,直到對該函數進行下一次迭代。
 
可能讀到這些解釋,有的讀者還是不太明白,什麼時候進行下一次迭代呢?如果不使用for…in…語句,是否可以對產生器進行迭代呢?其實迭代器有一個特殊方法__next__。每次對迭代器的迭代,本質上都是在調用__next__方法。
 
那麼還有最後一個問題,for…in…語句在什麼時候才會停止迭代呢?其實for…in…語句在底層會不斷調用in子句後面的可迭代對象的__next__方法,直到該方法拋出StopIteration異常為止。也就是說,可以將上面的for…in…循環改成下面的程式碼。連續調用6次__next__方法,返回0到10,一共6個偶數,當第7次調用__next__方法時,產生器中已經沒有值了,所以會拋出StopIteration異常。由於for…in…語句自動處理了StopIteration異常,所以循環就會自動停止,但當直接調用__next__方法時,如果產生器中沒有值了,就會直接拋出StopIteration異常,除非使用try…except…語句捕獲該異常,否則程式會異常中斷。
even_generator = generate_even(10)
print(even_generator.__next__())
print(even_generator.__next__())
print(even_generator.__next__())
print(even_generator.__next__())
print(even_generator.__next__())
print(even_generator.__next__())
# print(even_generator.__next__())  # 拋出StopIteration異常

總結:產生器本質上就是動態產生待迭代的值,使用完就直接扔掉了,這樣非常節省記憶體空間,但這些值只能被迭代一次。

 
4. 用普通函數模擬產生器函數的效果
 
如果你看到一個函數中使用了yield語句,說明該函數是一個產生器。其實可以按下面的步驟將該產生器函數改造成普通函數。
 
1. 在函數的開始部分定義一個列表變數,程式碼如下:

result = []
2. 將所有的yield expr語句都替換成下面的語句:
result.append(expr)
3. 函數的最後執行return result返回這個列表對象
 
 
為了更清晰表明這個轉換過程,現在給出一個實際的案例:
 
# 產生不大於max的偶數
def generate_even(max):
    for i in range(0, max + 1):
        if i % 2 == 0:
            yield i

even_generator = generate_even(10)
for n in even_generator:
    print(n, end=' ')

# 將產生器函數改造成普通函數,實際上,就是將yield後面表達式的值都添加在列表中
def generate_even1(max):
    evens = []
    for i in range(0, max + 1):
        if i % 2 == 0:
            evens.append(i)
    return evens
print()
even_list = generate_even1(10)
for n in even_list:
    print(n, end=' ')

在這段程式碼中有兩個函數:generate_even和generate_even1,其中generate_even是產生器函數,generate_even1是普通函數(與generate_even函數的功能完全相同)。按著前面的步驟,將所有產生的偶數都添加到了列表變數evens中,最後返回這個列表變數。這兩個函數在使用方式上完全相同。不過從本質上說,generate_even函數是動態生成的偶數,用完了就扔,而generate_even1函數事先將所有產生的偶數都添加到列表中,最後返回。所以從generate_even1函數的改造過程來看,yield的作用就相當於使用append方法將表達式的值添加到列表中,只不過yield並不會保存表達式的值,而append方法會保存表達式的值。

 
 5.與迭代相關的API
 
這一節來看一看Python為我們提供了哪些與迭代相關的API
 
Python SDK提供了一個itertools模組,該模組中的API都與迭代相關,例如,可以通過chain.from_iterable方法合併多個可迭代對象,通過permutations函數以可迭代對象形式返回列表的全排列。
from itertools import *

# 這裡每一個yield的值必須是可迭代的,才能用chain.from_iterable方法合併
def make_iterables_to_chain():
    yield [1,2,3]
    yield ['a','b','c']
    yield ['hello','world']

for v in make_iterables_to_chain():
    print(v)
# 將所有可迭代對象合併成一個可迭代對象
for v in chain.from_iterable(make_iterables_to_chain()):
    print('<',v,'>', end = ' ')
print('-------上面的程式碼相當於下面的寫法-------')
a = [1,2,3]
a.extend(['a','b','c'])
a.extend(['hello','world'])
print(a)
for v in a:
    print('[',v,']', end = ' ')
#  以可迭代對象形式返回列表的全排列
values = [1,2,3,4]
values_permutations = permutations(values)
for p in values_permutations:
    print(p)

 

執行這段程式碼,會輸出如下內容:
[1, 2, 3]
['a', 'b', 'c']
['hello', 'world']
< 1 > < 2 > < 3 > < a > < b > < c > < hello > < world > -------上面的程式碼相當於下面的寫法-------
[1, 2, 3, 'a', 'b', 'c', 'hello', 'world']
[ 1 ] [ 2 ] [ 3 ] [ a ] [ b ] [ c ] [ hello ] [ world ] (1, 2, 3, 4)
(1, 2, 4, 3)
(1, 3, 2, 4)
(1, 3, 4, 2)
(1, 4, 2, 3)
(1, 4, 3, 2)
(2, 1, 3, 4)
(2, 1, 4, 3)
(2, 3, 1, 4)
(2, 3, 4, 1)
(2, 4, 1, 3)
(2, 4, 3, 1)
(3, 1, 2, 4)
(3, 1, 4, 2)
(3, 2, 1, 4)
(3, 2, 4, 1)
(3, 4, 1, 2)
(3, 4, 2, 1)
(4, 1, 2, 3)
(4, 1, 3, 2)
(4, 2, 1, 3)
(4, 2, 3, 1)
(4, 3, 1, 2)
(4, 3, 2, 1)

 

 更多免費影片課程,請識別下面二維碼看我的B站

 

 

對本文感興趣,可以加李寧老師微信公眾號(unitymarvel)

 

 

關注  「極客起源」  公眾號,獲得更多免費技術影片和文章。