連Python產生器(Generator)的原理都解釋不了,還敢說Python用了5年?
阿新 • • 發佈:2020-10-12
最近有很多學Python同學問我,Python Generator到底是什麼東西,如何理解和使用。Ok,現在就用這篇文章對Python Generator做一個敲骨瀝髓的深入解析。 為了更好地理解產生器(Generator),還需要掌握另外兩個東西:yield和迭代(iterables)。下面就迭代、產生器和yield分別做一個深入的解析。 1. 迭代 當建立一個列表物件後,可以一個接一個讀取列表中的值,這個過程就叫做迭代。
mylist = [1, 2, 3] for i in mylist: print(i, end = ' ')
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=' ')
執行這段程式碼,會輸出如下結果:
<generator object <genexpr> at 0x7f95e0154150> 0 1 4 第二次迭代data_generator,什麼都不會輸出
# 編寫產生器函式 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)
關注 「極客起源」 公眾號,獲得更多免費技術視訊和文章。