[Python]深入理解容器、迭代器與生成器
一、什麼是iterable?
理解生成迭代器和生成器之前,先了解可迭代對象。
1.官方文檔的定義:
An object capable of returning its members one at a time. 可迭代的對象
2.可迭代的對象包含類型:
- 所有的序列類型(sequence type):比如列表、字元串、元組等;
from collections import Iterableprint(isinstance([1, 2, 3], Iterable)) # Trueprint(isinstance("hello python", Iterable)) # Trueprint(isinstance((1, 2, 3), Iterable)) # True
- 一些無序列的類型( non-sequence type):比如字典、文件對象;
from collections import Iterableprint(isinstance({"language": "Python"}, Iterable)) # True
- 自定義的類,但結構內需包含_iter()方法或者_getitem_()方法
from collections import Iterableclass NumberList(): """定義一個數字類""" def __init__(self): super(NumberList, self).__init__() self.__number = list() def number_append(self, item): """向空列表中添加數字""" self.__number.append(item) def __iter__(self): pass# 實例化number_list = NumberList()# 向列表中添加幾個數字number_list.number_append(1)number_list.number_append(2)number_list.number_append(3)# 查看number_list是否是Iterable類型print(isinstance(number_list, Iterable)) # 結果為True
3.取值方式:
- 方式一:使用for循環可以遍歷取值。
for i in [1, 2, 3]: print(i)
- 方式二:使用方法iter()
說明1:當一個iterable對象作為參數傳給iter()函數時,會返回一個iterator對象。
from collections import Iterable, Iteratorl = [1, 2, 3]print(isinstance(l, Iterable)) # Trueprint(isinstance(l, Iterator)) # Falseit = iter(l) # 迭代器對象print(isinstance(it, Iterable)) # Trueprint(isinstance(it, Iterator)) # True,此時it已經是一個Iterator對象了。
說明2:Iterator對象會有一個_next_()方法或者調用Python內置的函數方法next()。以下三個方法不要同時執行,運行一個注釋其它兩個。
# 方法1:for語句for i in it: print(i)# 方法2:while循環while True: try: print(next(it)) except StopIteration: break# 方法3:__next__()方法while True: try: print(it.__next__()) except StopIteration: break
注意:當我們使用iterable對象時,通常沒有必要調用iter()函數返回iterator對象或者自己去處理iterator對象。通常for就能解決這個問題,而且簡潔明了。for語句內部機制會創建一個未命名的臨時變數,用於接受迭代器對象。
二、什麼是itreator?
1.官方文檔的定義:
An object representing a stream of data.
iterator表現為數據流(stream of data)的形式的對象,而像list、tuple、dict等對象,表現為容器對象(container).
在上文,已經了解了iterable對象,如果要遍歷取值的話,一般通過for語句的內部機制,將iterable對象轉為iterator對象,再取值。其實,對於像列表、元組、字元串等這樣的iterable對象只有__iter__()方法,而沒有__next__()方法,所以說它們都是可迭代的,即具有迭代的天賦,但如果給它們用函數iter()升級一下,它們就變成了iterator對象,就具備了__next__()。所以兩者區別之一,是否有__next__()方法。
注意1:
對container對象使用iter()方法或者使用for語句的時候,每次都是生成一個新的迭代器。也就是說,你每次對同一個container對象進行上述調用,返回的iterator對象是不一樣的(內存中地址不一樣)。
注意2:
對container對象使用iter()方法或者for語句生成的iterator操作,即使取盡了iterator中的所有值,也不會對container對象產生影響,只是讓你看上去container好像空了,好比對某個列表進行上述操作,並不影響該列表裡的值。
2.取值:
通過調用迭代器的__next__()方法或者把迭代器作為參數傳給函數next(),這兩種方法都會返回數據流中連續值。當數據流中的值取光後,再取值會rasie StopIteration。
3.自定義類CharList迭代器:
from collections import Iteratorclass CharList(): """定義一個字元類""" def __init__(self): super(CharList, self).__init__() self.__char_list = list() self.__current_index = 0 # 默認下標為0 def show_list(self): """查看list中的元素""" print(self.__char_list) def char_append(self, item): """向空列表中添加數字""" self.__char_list.append(item) def __iter__(self): """返回迭代器對象""" return self def __next__(self): """獲取迭代器對象中下一個值""" if self.__current_index < len(self.__char_list): # 如果當前index在列表長度範圍內 self.__current_index += 1 return self.__char_list[self.__current_index - 1] else: # 表示index越界,拋出停止迭代的異常 raise StopIteration# 實例化char_list = CharList()# 向列表中添加幾個數字char_list.char_append("Hello")char_list.char_append("Python")char_list.char_append("!")# 查看char_list是否是Iterator類型print(isinstance(char_list, Iterator)) # 結果為True# 取出所有值for item in char_list: print(item)# 查看CharList中是否還有值char_list.show_list()
執行結果:
TrueHelloPython![Hello, Python, !]
說明:在上述例子自定義類迭代器中,即使取盡了迭代器中的值,但char_list中的值還是沒有改變,因為char_list就跟container對象一樣,貌似看上去變成了空容器。
Attempting this with an iterator will just return the same exhausted iterator object used
in the previous iteration pass, making it appear like an empty container.(官方文檔給出的解釋)
4.使用迭代器完成斐波拉切數列:
class Fibonacci(): """定義一個斐波拉切數列類""" def __init__(self, a, b, total): """ 初始化斐波拉切數列 :param a: 第一個數字 :param b: 第二個數字 :param total: 需要產生的數字個數 """ self.a = a self.b = b self.total = total self.current_index = 0 # 初始index默認為0 def __iter__(self): """返回迭代器對象""" return self def __next__(self): """獲取下一個數""" # 判斷index是否越界 if self.current_index < self.total: self.a, self.b = self.b, self.a + self.b self.current_index += 1 return self.a else: raise StopIterationfibonacci = Fibonacci(0, 1, 5)for i in fibonacci: print(i)
5.迭代器有什麼用?
為什麼要有迭代器呢?這個問題估計好多人沒有想過,我查看了文檔,也沒有作出解釋。如果將container、iterator、generator(生成器)這三個結合思考不難發現,container對象,比如list、dict、file這些對象,都是會實際存儲數據的,有時候存的少,有時候存的多,這樣會很佔資源。如果需要使用時,會全部載入到內存,很形象地像容器一樣可以裝很多時候。
container存儲的是實際數據,那iterator存儲的是什麼呢?是演算法。比如,上面定義的兩個類迭代器,都是先定義運算規則,並沒有存儲需要的實際數據,當你有需要的時候,只需要通過for語句或者next()方法取出來即可。而generator本質上也是iterator,是一種特殊的iterator,中文名翻譯的也很形象,「生成器」,數據一個一個地「蹦出來」。
三、什麼是generator?
1.什麼是generator?什麼是generator iterator?
generator官方文檔定義:
A function which returns a generator iterator. It looks like anormal function except that it contains yield
expressions for producing a series of values usable in a for-loop or that can be retrieved one at a time with the next()
function.
generator iterator官方文檔定義:
An object created by a generator function.
說明:前者表示函數,後者表示返回的特殊的迭代器對象。在不需要明確區分的時候,這兩個概念很容易被混用,比如用generator表示生成器對象。
2.什麼是generator expression?
An expression that returns an iterator.這是一種創建generator的方式之一。
比較上圖三行代碼可以發現,它們唯一的區別在於外面的括弧類型不一樣,使用方括弧的是列表解析式,用花括弧的是集合解析式,這兩個返回的分別是列表、集合,這兩個都是container對象。第三個用的是圓括弧,返回的是generator的內存地址,這個就是generator expression,生成器表達式。生成器表達式,可以返回一個迭代器對象。
說明:上面這三種表達方式都很緊湊,在某些情況下使用能使代碼更簡潔。
如何取值?作為一種特殊的迭代器,同樣可以使用next()方法或者for語句取值。
- next()方法:
- for語句:
說明:next()取完所有值後如果再取,會報錯StopIteration,停止迭代。for語句內部有捕獲異常的機制,而且for語句使用起來更方便。
3.什麼是yield expressions?
上文使用了generator expression創建了一個生成器對象,這種方式雖說及其緊湊方便,但是很多情況不能用generator expression表示,這時候需要用到更為強大的yield expressions。
yield expressions可以用於一般的生成器函數(generator function)或者是非同步生成器函數(asynchronous generator function,用於協程)。只要函數體內包含yield關鍵字,普通函數就變成了生成器函數。
使用yield,創建一個簡單的生成器函數
def generator_func(): yield 123# 由於生成器是特殊的迭代器,生成器一樣存儲的是演算法,需要的時候next()調用即可g = generator_func() print(next(g)) # 123
4.比較return與yield的執行機制:
# 這是包含return的普通函數def test_1(num): for i in range(num): print("--------%d--------" % i) return i print("++++++++%d++++++++" % i)# 這是包含yield的生成器函數def test_2(num): for i in range(num): print("--------%d--------" % i) yield i print("++++++++%d++++++++" % i)f = test_1(3)print(f)print(type(test_1)) # <class function>print("=" * 50)g = test_2(3)print(g) # <generator object test_2 at 0x7fc1658f7af0>print(type(g)) # <class generator>for i in g: print(i)
運行結果:
--------0--------0<class function>==================================================<generator object test_2 at 0x7fc1658f7af0><class generator>--------0--------0++++++++0++++++++--------1--------1++++++++1++++++++--------2--------2++++++++2++++++++
比較上面代碼:定義的兩個函數幾乎一樣,唯一不同之處,一個使用了return關鍵字,一個使用的是yield關鍵字。可是執行結果卻有很大不同:①使用return定義的那個函數是普通函數,使用yield定義的函數是生成器函數;②只含return的函數,沒有遍歷所有值,return語句後的代碼沒有執行,直接向父函數提交返回值;含yield的函數,取到了生成器中的所有值。
以上兩者的不同,只是從代碼執行結果比較出來的,那麼return與yield的內部執行機制有什麼不同呢?
兩者的對所在線程(Thread)的控制權是不一樣的。在return所在的函數中,只要執行到return語句後,return之後的語句都不會執行,因為執行完return語句後,立即交出對線程的控制權,也就是說,在同一函數中,return之後的語句都不佔用線程資源。
而yield生成器函數,在執行到yield語句後,會像return一樣把返回值交給調用函數,不同的是,在提交完返回值之後yield整個生成器都處於暫停狀態,這時候跟return一樣會交出線程的控制權,yield後面的代碼也不會執行,如果想執行yield後面的語句,只能再次佔有線程。如果你不讓它重新佔有線程(即恢復運行狀態)的話,它會一直處於暫停狀態,yield語句後生成器就是這麼「懶」,一點都不主動。你可能會有疑問,我既沒有看到它暫停的狀態,也沒有看到將它從暫停狀態恢復到運行狀態?別忘了,生成器是特殊的迭代器,迭代器可以通過for語句或者next()函數進行取值,上面的代碼正是有了for語句,而不需要你主動去恢復它運行狀態。如果你next()函數一次一次取值的話,你就會發現它有多懶。
下面通過一個例子,來仔細看看yield的內部執行機制:
# import timedef test_1(): for i in range(5): yield i # time.sleep(0.5)def test_2(): for i in range(5, 10): yield i # time.sleep(0.5)g1 = test_1()g2 = test_2()while True: try: print(next(g1)) print(next(g2)) except StopIteration: break
運行結果:
0516273849
定義了兩個yield生成器函數,執行結果,並沒有按照0-9的順序依次顯示出來,而是交替顯示的。為什麼呢?因為在同一線程下,這兩個生成器在同一時候只能有一個函數擁有線程資源。當g1擁有線程式控制制權(ge只能「伺機而動」),執行到yield語句提交返回值的同時,交出了線程的控制權。這時候g2生成器立刻意識到了線程處於空閑狀態,取得線程的控制權。就這樣,交替執行。由於CPU執行速度太快,你根本察覺不到從暫停到喚起的時間差,但是你能從上個例子的結果看到代碼執行的順序。
那為什麼說yield生成器函數強大?有什麼用呢?
yield生成器函數可以實現非同步,用於協程(Coroutine)多任務。在cpython解釋器下的Python由於有一個GIL(全局解釋器鎖)的存在,使用多線程實現的多任務其實是偽多任務,因為始終是在單核CPU上執行的,不能充分發揮多核CPU。儘管協程處於線程內,但是協程比線程佔用更少的資源,Python中進程+協程的組合會比進程+線程的組合更節省資源。
5.generator-iterator對象有哪些方法?
生成器對象像Python中其他object一樣,也有一些方法。
generator.__next__
():
作用:該方法的作用與next()函數一樣,用於取迭代器下一個值
說明:可以start(開啟)或者resume(恢復)生成器的執行狀態;也就是說可以用它取第一個值,或者從暫定狀態處恢復執行狀態並向下取一個值。
generator.send
(value):
作用:當生成器處於暫停狀態時,像生成器傳一個值,也具有resume功能。
注意:當還沒有start生成器(還沒有調用過generator.__next__()或者next()函數時),是不能直接使用send()方法傳參數的,只能先調用一下next()方法(或者使用send(None),兩者等價)。
generator.throw
(type[, value[, traceback]]):
作用:拋出各種類型的異常
generator.close
()
作用:當generator處於暫停狀態時,可以使用該狀態主動關閉生成器。當再次使用該生成器對象時,會拋出停止迭代的異常,可以清理乾淨內存中的生成器對象。
四、小結
1.container對象:像list、tuple、str、dict、file等對象,在數據存儲的形式就像容器一樣;
2.iterable對象:Python提供了不少Iterable對象,包括所有的序列類型(如列表),一些無序列類型(如字典、文件等),同時也可以通過自定義類的方法實現可迭代的類對象。所有的iterable對象,都具有共同的一個方法__iter__()
3.iterator對象:迭代器對象,不同於container的數據存儲形式,iterator只存儲演算法的內存地址,iterator對象與iterable對象不同之處在於,iterator同時具有__iter__()和__next__()方法。儘管像列表、元祖這樣的對象不具有__next__()方法,但是可以通過iter()方法或者for語句為它們返回一個迭代器對象。
4.generator對象:生成器對象是特殊的迭代器,具有迭代器一切的特點。創建generator對象一般有兩種方法,一種是generator expression生成器表達式,形式上與列表、集合解析式很像,實質卻完全不同;另一種創建方式yield expressions,通過自定義generator function,能夠發揮生成器更大的作用。yield expressions的靈活準確應用,需要建立在深刻理解yield關鍵字的內部執行機制上。
5.yield表達式的非同步執行,可以用於協程,實現多任務。
推薦閱讀: