1. 程式人生 > >Python學習之旅—Day07(生成器與叠代器)

Python學習之旅—Day07(生成器與叠代器)

討論 三次 iterable 結果 fis post 工作 映射 我們

前言

    本篇博客主要專註於解決函數中的一個重要知識點——生成器與叠代器。不管是面試還是工作,生成器與叠代器在實際工作中的運用可以說是非常多,從我們第一天開始學習for循環來遍歷字典,列表等數據類型時,我們就已經和生成器,叠代器打交道了!本篇博客從最基礎的基本概念,例如容器,可叠代對象,生成器,叠代器的概念,到for循環是怎麽工作的娓娓道來。希望本篇博客能夠幫助大家切實掌握生成器與叠代器的使用與底層原理。


一.容器

   容器是一種把多個元素組織在一起的數據結構,容器中的元素可以逐個地叠代獲取,可以用in, not in關鍵字判斷元素是否包含在容器中。那到底什麽是叠代呢?叠代就是從某個容器對象中逐個地讀取元素,直到容器中沒有更多元素為止

。我們在前面所學習過的列表,字典和集合等數據結構都是容器,它們將所有的元素都存儲在內存中;但是有一些特例,例如生成器和叠代器對象,它們並非將元素直接放在內存中。在Python中,一些常見的容器對象如下所示:

list, deque, ....
set, frozensets, ....
dict, defaultdict, OrderedDict, Counter, ....
tuple, namedtuple, …
str

    為了方便各位同學理解各種數據結構中的概念,這裏我借用網絡上的一張圖來表示他們的關系,如下所示:   

    技術分享    容器比較好理解,我們可以將它看作是一個箱子,可以往裏面塞任何東西。從技術上而言,當我們需要查詢某個元素是否在某個對象中時,此時這個對象就是一個容器,例如我們前面學過的字符串,字典,列表,元組等都可以看作是一個容器,我們都可以使用in 或者not in來判斷某個元素在或者不在其中,一起來看看下面的代碼:

s = "spark is very important"
if spark in s:
    print("spark is in s!!")
else:
    print("spark is not in s")

item_list = [1, 2, 3, "spark", "python"]
if "hadoop" not in item_list:
    print("hadoop is not in item_list")
else:
    print("hadoop is in item_list")

item_tuple = (1, "spark", "hadoop
") if "python" not in item_list: print("python is not in item_tuple") else: print("python is in item_tuple") column_dic = {id: 0, name: 1, age: 2, phone: 3, job: 4} if gender in column_dic.keys(): print("gender is in column_dic‘keys") else: print("gender is not in column_dic‘keys")
打印結果如下:

spark is in s!!
hadoop is not in item_list
python is in item_tuple
gender is not in column_dic‘keys

盡管絕大多數容器都提供了某種方式來獲取其中的每一個元素,但這並不是容器本身提供的能力,而是可叠代對象賦予了容器這種能力。這裏要特別說明的是並不是所有的容器都是可叠代的,例如:Bloom filter,雖然Bloom filter可以用來檢測某個元素是否包含在容器中,但是並不能從容器中獲取其中的每一個值,因為Bloom filter壓根就沒把元素存儲在容器中,而是通過一個散列函數映射成一個值保存在數組中。下面我們就來一起探討什麽事可叠代對象。


二.可叠代對象

   剛才我們說過,很多的數據類型都是可叠代對象,例如字符串,列表,字典等。另外也包含一些其他的對象,例如文件句柄,socket對象。通俗一點講,凡是實現了__iter__方法的對象就是可叠代對象,我們可以從中按一定次序提取出其中的元素。下面通過實際的代碼來講解:

from collections import Iterable  # 導入該模塊,用於檢測一個對象是否可叠代

print(isinstance(python, Iterable))  # 返回True
print(isinstance((123, 456), Iterable))  # 返回True
print(isinstance([1, 2, 3, "spark", "python"], Iterable))  # 返回True
通過上面程序運行結果分析可知,字符串,元組,列表等數據類型都是可叠代的,我們使用英文單詞Iterable來描述一個對象是否可叠代

剛才我們說過可叠代對象都實現了__iter__方法,那我們現在就來看看,如果一個可叠代對象調用__iter__方法會返回什麽?如下:

item_list = [1, 2, 3, "spark", "python"]
print(item_list.__iter__(), type(item_list.__iter__()))
# 打印 <list_iterator object at 0x000002C39C3B9780> <class ‘list_iterator‘>

從上面程序運行的結果可知,可叠代對象item_list調用__iter__方法返回的是一個list_iterator,從字面意思上我們可以翻譯為列表叠代器,那如果是元組和字典呢?我們繼續來看下面的代碼:

item_tuple = (1, "spark", "hadoop")
print(item_tuple.__iter__(), type(item_tuple.__iter__()))
# 打印 <tuple_iterator object at 0x000001E48E4E9470> <class ‘tuple_iterator‘>

column_dic = {id: 0, name: 1, age: 2, phone: 3, job: 4}
print(column_dic.__iter__(), type(column_dic.__iter__()))
# 打印 <dict_keyiterator object at 0x000001E48E380C78> <class ‘dict_keyiterator‘>

從運行結果可知,當我們分別使用元組和字典調用__iter__方法時,返回的分別是元組叠代器和字典叠代器,而且前面打印出元組叠代器和字典叠代器在內存中的地址值。因此我們可以得出結論:可叠代對象實現了__iter__方法,在可叠代對象上調用__iter__方法會返回一個叠代器對象(剛才我們打印出來叠代器對象在內存中的地址值),而且叠代器是一種統稱,具體而言,根據不同的可叠代對象,可以有列表叠代器,元組叠代器,字典叠代器等。下面我們就來具體探討下叠代器的概念與底層原理。


三.叠代器

   剛才我們講過,可叠代對象調用__iter__方法會返回一個叠代器對象。那到底什麽是叠代器呢?

   叠代器是一個帶狀態的對象,它能在你調用next()方法的時候返回容器中的下一個值,任何實現了__iter__和__next__()(python2中實現next())方法的對象都是叠代器,__iter__返回叠代器自身,__next__返回容器中的下一個值,如果容器中沒有更多元素了,則拋出StopIteration異常。

    為了方便大家更好地理解叠代器概念,我們首先來看看列表叠代器(list_iterator)比列表多了哪些方法。實現思路很簡單,使用dir命令求出列表叠代器和列表的內置方法,然後再變為集合,最後取差集,一起來看下面的代碼:

dir(lst_iterator)是列表叠代器lst_iterator實現的所有方法,dir(item_list)是列表中實現的所有方法,都是以列表的形式返回給我們的,為了更清楚地觀察它們,
我們分別把他們轉換成集合,然後取差集。
item_list = [1, 2, 3, "spark", "python"]
lst_iterator = item_list.__iter__()
print(set(dir(lst_iterator)) - set(dir(item_list)))
# 打印 {‘__setstate__‘, ‘__length_hint__‘, ‘__next__‘}

從運行結果可知,列表叠代器比列表多了3個方法,那這3個方法究竟有什麽用呢?來看看下面的代碼:

item_list = [1, 2, 3, "spark", "python"]
lst_iterator = item_list.__iter__()

# __length_hint__()方法返回叠代器中元素個數
print(lst_iterator.__length_hint__())  # 返回 5

# __setstate__(2)方法表示根據索引值指定從哪裏開始叠代,例如下面指定從索引為2的位置開始叠代
print(****, lst_iterator.__setstate__(2))  # 返回 **** None

# __next__()方法,一個一個去列表中取元素
print("[001]", lst_iterator.__next__())  # 打印 [001] 3
print("[002]", lst_iterator.__next__())  # 打印 [002] spark
print("[003]", lst_iterator.__next__())  # 打印 [003] python
print("[004]", lst_iterator.__next__())  # 會報異常:StopIteration
# 在上面執行第4個打印語句時候,會報異常
StopIteration,因為在這裏我們通過
__setstate__(2)指定了從索引為2的位置開始叠代獲取元素,所以
# 元素3開始打印

  通過剛才的實驗,我們是通過__next__()方法不斷從叠代器中獲取元素。因此結合前面的知識,我們可以得出結論:任何實現了__iter__方法的對象都是可叠代的,可叠代對象調用__iter__()方法會返回一個叠代器對象,而叠代器對象通過__next__()方法不斷從叠代獲取叠代器對象裏面的元素。所以我們說只要實現了__iter__和__next__()(python2中實現next())方法的對象都是叠代器,__iter__返回叠代器自身,__next__返回容器中的下一個值。

  既然我們知道了叠代器的本質,下面我們嘗試通過叠代器的__iter__和__next__方法來寫一個不依賴於for循環的遍歷,如下所示:

item_list = [1, 2, 3, "spark", "python"]
lst_iterator = item_list.__iter__()   # 調用__iter__()方法將可叠代對象變為叠代器對象

item = lst_iterator.__next__()  # 調用叠代器的__next__()方法來獲取列表中的元素
print(item) #打印 1

item = lst_iterator.__next__()
print(item) #打印2

item = lst_iterator.__next__()
print(item) #打印3

item = lst_iterator.__next__()
print(item) #打印spark

item = lst_iterator.__next__()
print(item) #打印python

item = lst_iterator.__next__()  # 會報異常
print(item)

通過程序的運行結果可知,最後一行會報異常,因為列表所有的元素已經被叠代訪問完畢,此時沒有可叠代的元素了,為了處理該異常,我們使用後面的異常處理機制,代碼如下:

item_list = [1, 2, 3, "spark", "python"]
lst_iterator = item_list.__iter__()   # 調用__iter__()方法將可叠代對象變為叠代器對象
while True:
    try:
        """
        通過while循環不斷調用__next__()方法來獲取列表中的元素,直到列表中的元素都被叠代訪問完畢。
        """
        item = lst_iterator.__next__()
        print(item)
        # 當繼續叠代訪問列表中的元素時,此時會拋出異常StopIteration,然後使用break關鍵字結束循環。
    except StopIteration:
        break

  通過上面的代碼,可知:一旦叠代器建立起來,提取元素的過程就不再依賴於原始的可叠代對象,而是僅僅依賴於叠代器本身。我們通過調用叠代器對象的___next__()方法,會執行三個操作:

  【001】返回當前『位置』的元素,第一次調用___next__()方法,當前位置是可叠代對象的起始位置

  【002】將『位置』向後遞增

  【003】如果到達可叠代對象的末尾,即沒有元素可以提取,則拋出StopIteration異常

  在實際工作中,我們並不需要這麽麻煩來使用叠代器遍歷可叠代對象中的元素,Python中的循環語句會自動進行叠代器的建立、next的調用和StopIteration的處理。換句話說,遍歷一個可叠代對象的元素,我們這樣寫就行了:

item_list = [1, 2, 3, "spark", "python"]
for item in item_list:
    print(item)

我們之所以可以如此方便快捷地使用for......in語句遍歷可叠代對象,是因為該語句隱藏和實現了上面的細節。是不是感覺豁然開朗?

   再回到我們之前的問題,Python 2.X 的 range和xrange有何區別?答案是,range的返回值就是一個list,在我們調用range的時候,Python會產生所有的元素。而xrange是一個特別設計的可叠代對象,它在建立的時候僅僅保存終止值,需要使用的時候再一個個地返回,它不會像range一樣,一下子將所有的元素都放入內存,這樣就很好地節省了內存,避免了內存溢出等錯誤。在Python 3.X 中,不再有內建的xrange,其range等效於Python 2.X 的xrange。


四.自定義叠代器

  前面我們探討了可叠代對象和叠代器的本質,即凡是實現了__iter__()方法的對象都是可叠代對象,凡是實現了__iter__()方法和__next__()方法的對象都是叠代器。那我們可以自定義叠代器嗎?帶著這個問題,我們從0開始來設計一個叠代器,再次從本質上理解叠代器的概念。

class MyIterator:
    def __init__(self, num):
        self.num = num

for i in MyIterator(20):
    print(i)
# 報錯:TypeError: ‘MyIterator‘ object is not iterable

從報錯信息可知:由於MyIterator(10)對象不是一個可叠代的對象,因此它不能使用for循環進行叠代和遍歷。既然搞清楚了錯誤,我們可以在MyIterator類中實現__iter__和__next__()兩個方法,從而自定義一個叠代器,代碼如下:

class MyIterator:
    def __init__(self, num):
        self.i = 0
        self.num = num

    def __iter__(self):
        return self

    def __next__(self):
        if self.i < self.num:
            i = self.i
            self.i += 1
            return i
        else:
            # 達到某個條件時必須拋出此異常,否則會無止境地叠代下去
            raise StopIteration()

for i in MyIterator(10):
            print(i)

從代碼中可知,我們實現了__iter__和__next__()兩個方法,因此使用for循環進行叠代時,可以成功遍歷元素。通過這個示例可知,如果想要使用for......in成功遍歷我們自定義的叠代器,只需要實現相應的叠代器接口即可,即剛才提到的兩個方法。


五.生成器

  通過前面的知識我們了解了可叠代對象和叠代器的本質,本小節我們專註於討論生成器的底層原理和使用。我們知道的叠代器有兩種:一種是調用方法直接返回的,一種是可叠代對象通過執行iter方法得到的,叠代器的好處是可以節省內存。在某些情況下,我們也需要節省內存,就只能自己寫。我們自己寫的這個能實現叠代器功能的東西就叫生成器。

  在Python中,生成器分為兩種:1.生成器函數,2.生成器表達式。下面我們來重點討論生成器函數。

  我們知道普通函數通常使用 return 返回一個值,這和 Java 等其他語言是一樣的。然而在 Python 中還有一種函數,用關鍵字 yield 來返回值,這種函數叫生成器函數,函數被調用時會返回一個生成器對象,生成器本質上還是一個叠代器,也是用在叠代操作中,因此它有和叠代器一樣的特性,唯一的區別在於實現方式上不一樣,後者更加簡潔

  因此,我們可以概括生成器函數的定義:凡是包含了yield關鍵字的函數就是一個生成器函數。yield可以為我們從函數中返回值,但是yield又不同於return,return的執行意味著程序的結束,調用生成器函數不會得到返回的具體的值,而是得到一個生成器對象,它也不會執行生成器函數中的具體內容。每次調用生成器對象的__next__()方法才會獲取到生成器函數具體的返回值。我們首先通過一段簡短的代碼來說明生成器函數:

def generator_func():  # 帶yield關鍵字的函數就是生成器函數
    print(123)
    yield aaa  # 這裏的yield相當於return關鍵詞,只有執行__next__()方法時,才執行生成器函數裏面的函數體
    sum_count = 1 + 2
    yield sum_count
    a = 666
    yield a

g = generator_func() 
print(g) #打印<generator object generator_func at 0x000001D49362BE08>

從上面的運行結果可知:直接調用生成器函數,它返回的是一個生成器對象,我們直接打印該對象,可以獲取該對象在內存中的地址值:0x000001D49362BE08。此時調用生成器函數,壓根就沒有執行生成器函數裏面的內容,那如果我們想要獲取生成器函數中的返回值該怎麽辦?直接調用

__next__()方法即可。來看下面的代碼:

def generator_func():  # 帶yield關鍵字的函數就是生成器函數
    print(123)
    yield aaa  # 這裏的yield相當於return關鍵詞,只有執行__next__()方法時,才執行生成器函數裏面的函數體
    sum_count = 1 + 2
    yield sum_count
    a = 666
    yield a

g = generator_func()  # 生成器函數在執行的時候返回的是一個生成器對象
result = g.__next__()  # __next__()啟動生成器函數
print(result)  # 打印出123 和aaa

可以看到當我們開始調用__next__()方法,才返回生成器函數的返回值,而且此時生成器函數中的函數體內容執行到yield ‘aaa‘就暫時終止了,打印結果說明了這點。

由此可知當我們調用生成器對象的__next__()方法時,生成器函數的函數體執行到第一個yield關鍵字暫時停止,如果我們想繼續獲取生成器函數中使用yield關鍵字返回的值,該怎麽辦?到這裏我想大家應該明白了,我們只需要繼續調用__next__()方法即可,如下:

def generator_func():  # 帶yield關鍵字的函數就是生成器函數
    print(123)
    yield aaa  # 這裏的yield相當於return關鍵詞,只有執行__next__()方法時,才執行生成器函數裏面的函數體
    sum_count = 1 + 2
    yield sum_count
    a = 666
    yield a

g = generator_func()  # 生成器函數在執行的時候返回的是一個生成器對象

result = g.__next__()  # __next__()啟動生成器函數
print(result)  # 打印出123 和aaa

result = g.__next__()  # __next__()啟動生成器函數
print(result)  # 打印出3

sum_count = g.__next__()
print(sum_count) #打印出666

sum_count = g.__next__()
print(sum_count)  # 報異常:StopIteration

根據上面的運行結果可知,最後一個打印結果報異常,我想大家應該能夠猜出具體原因了,因為生成器函數中使用三個yield關鍵字來返回值,而我們在外部卻使用四個g.__next__()來叠代獲取元素,當前面三個返回值被叠代獲取完畢後,已經沒有元素了,因此當使用第四個g.__next__()獲取返回值時,肯定會報異常。

   既然我們生成器的本質是叠代器,因此我們也可以直接for循環來遍歷生成器對象,從而獲取生成器函數中的返回值。代碼如下:

def generator_func():
    print(123)
    yieldaaaprint(456)
    yieldbbb
g = generator_func()
for i in g:
    print(i : ,i)
#打印值如下:
123 i : aaa 456 i : bbb

在上面的程序中,我們通過循環遍歷生成器對象g,然後不斷獲取生成器函數中由yield返回的值。這裏就相當於執行了g.__next__()方法,只是這次不會出現異常:

StopIteration。我們再來看下面這段代碼,看它會打印什麽結果:

def generator_func():
    count = 1
    while True:
        yield count
        count += 1
g = generator_func()
for i in g:
    print(i : , i)
# 不斷地進行死循環,永遠跳不出,因為for i in g是不斷的遍歷生成器對象,並調用__iter__()方法不斷獲取由yield關鍵字返回的函數的值。
# 由於生成器函數中是一個死循環,yield不斷地返回值,因此外層不斷調用__iter__()方法來獲取返回值,這樣就陷入了一個惡性死循環。

我們再來觀察下面的代碼,各位可以嘗試觀察下面代碼與上面代碼的區別:

def generator_func():
    count = 1
    while True:
        yield count
        count += 1

g = generator_func()
for i in range(50):
    print(i : , g.__next__())
# 打印值從i:1 到i:50

我們一起來分析下上面兩段程序的區別:for i in range(50)來控制下面g.__next__()的執行次數,表明從生成器中返回50個值;而for i in g表示不斷叠代遍歷生成器對象g,如果生成器函數的函數體是死循環,那麽這段程序將永無止境的運行下去。

我們繼續來觀察下面這段代碼,進一步來分析下結果:

def get_clothing():
    for cloth in range(1, 2000000):
        yield第%s件衣服 % cloth

generate = get_clothing()
print(generate.__next__())
print(generate.__next__())
print(generate.__next__())
# 打印結果如下:
第1件衣服 第2件衣服 第3件衣服

如果我想打印50件衣服,那豈不是要寫50個print語句,這肯定不行,為此,我們使用range來控制次數,接著上面的代碼:

def get_clothing():
    for cloth in range(1, 2000000):
        yield第%s件衣服 % cloth

generate = get_clothing()
print(generate.__next__())
print(generate.__next__())
print(generate.__next__())

for i in range(50):
    print(generate.__next__())
# 打印結果如下:
第1件衣服 ....... 第51件衣服 第52件衣服 第53件衣服

有小夥伴肯定會很納悶,明明我控制的是打印50件衣服,但是怎麽打印出53件衣服?理由很簡單:因為它們使用的是同一個生成器對象,在前面我們已經調用了三次generate.__next__(),此時指針指到了第三個位置末尾,那麽在下面我們直接調用generate.__next__(),相當於從第四個位置的元素開始打印,所以會出現53件衣服。

  針對上面的問題,我們繼續來看下面幾個生成器中比較容易出錯的點,希望能夠幫助大家進一步理解生成器的概念和原理。繼續看代碼:

def generator_func():
    print(123)
    yield aaa
    print(456)
    yield bbb

result1 = generator_func().__next__()
print(result1)   # 打印 123 aaa

result2 = generator_func().__next__()
print(result2)   # 打印 123 aaa

小夥伴們又要納悶了,為什麽沒有打印456和bbb呢?這是因為每當我們調用一次生成器函數就會生成一個生成器對象,在上面的代碼中,我們調用了2次生成器函數generator_func(),所以會生成兩個生成器函數對象,當我們第一次調用generator_func().__next__()時,會執行生成器函數中的內容,直到執行到第一個yield為止,然後打印print(result1),因此會打印出123和aaa;當我們第二次調用generator_func().__next__()時,又會重新產生一個新的生成器對象,並且會重新從頭開始執行生成器函數中的內容,此時開始執行後,又會執行到第一個yield為止,然後將值返回給result2,所以此時會打印出123和aaa。

關於生成器的最後一個問題,這裏也要做一個簡短的說明。既然普通函數的return關鍵字可以返回None,那麽yield後面可以不跟任何返回值嗎?帶著這個問題,我們一起來看看如下的代碼測試:

def generator():
    yield
    yield 2
g = generator()
for i in g:
    print(i)
# 打印結果如下:None和2

由結果我們分析可知,yield後面可以不跟任何值,但是這樣做毫無意義!我們一般結合yield和while循環使用。

關於生成器中相關重點內容就總結到這裏,最後再來梳理下關於生成器取值要註意的幾個點:

【001】通過__next__方法來獲取yield返回值,此時有幾個yield就可以取幾次值,如果超過了yield的個數,那麽會報異常,因為值都被取完了。

【002】由於生成器的本質還是叠代器,所以我們可以通過for循環來遍歷生成器,類似這種:for i in g

【003】我們也可以通過其他數據類型來取值,例如將生成器對象通過列表list強制轉換list(g),這樣返回的就是一個列表,我們直接遍歷列表中的元素即可。因為列表中此時裝載著生成器中的所有內容。


結語:

  關於函數生成器與叠代器的討論就到此為止,歡迎各位感興趣的朋友們與我交流!本博客在寫的過程中參考了網絡上幾篇非常優秀的文章,現在分享出來,希望能夠對各位有所幫助!

【001】看完這篇,你就知道Python生成器是什麽:https://foofish.net/what-is-python-generator.html

【002】for循環在Python中是怎麽工作的:https://foofish.net/how-for-works-in-python.html

【003】完全理解Python叠代對象、叠代器、生成器:https://foofish.net/iterators-vs-generators.html

【004】Iterables vs. Iterators vs. Generators:http://nvie.com/posts/iterators-vs-generators/

【005】python之路——叠代器和生成器:http://www.cnblogs.com/Eva-J/articles/7213953.html#_label3

【006】Python 的叠代器和生成器:https://zhuanlan.zhihu.com/p/24499698

【007】黃哥漫談Python 生成器:https://zhuanlan.zhihu.com/p/21659182?refer=pythonpx

技術分享

Python學習之旅—Day07(生成器與叠代器)