1. 程式人生 > 實用技巧 >連Python產生器(Generator)的原理都解釋不了,還敢說Python用了5年?

連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)