1. 程式人生 > >詳解生成器、迭代器

詳解生成器、迭代器

原文:http://www.cnblogs.com/vipchenwei/p/6991204.html

 

 本文講述了以下幾個方面:

  1.何為迭代,何為可迭代物件,何為生成器,何為迭代器?

  2.可迭代物件與迭代器之間的區別

  3.生成器內部原理解析,for迴圈迭代內部原理解析

  4.可迭代物件,迭代器,生成器,生成器函式之間關係

1.迭代  

  要搞清楚什麼關於迭代器,生成器,可迭代物件,前提是我們要理解何為迭代。

  第一,迭代需要重複進行某一操作

  第二,本次迭代的要依賴上一次的結果繼續往下做,如果中途有任何停頓,都不能算是迭代.

  下面來看看幾個例子,你就會更能理解迭代的含義。

# example1
# 非迭代
count = 0
while count < 10:
    print("hello world")
    count += 1
# example2
# 迭代
count = 0
while count < 10:
    print(count)
    count += 1

  例子1,僅僅只是在重複一件事,那就是不停的列印"hello world",並且,這個列印的結果並不依賴上一次輸出的值。而例子2,就很好地說明迭代的含義,重複+繼續。

2.可迭代物件

  按照上面迭代的含義,我們應該能夠知道何為可迭代物件。顧名思義,就是一個物件能夠被迭代的使用。那麼我們該如何判斷一個物件是否可迭代呢?

  Python提供了模組collections,其中有一個isinstance(obj,string)的函式,可以判斷一個物件是否為可迭代物件。看下面例項:

from collections import Iterable

f = open('a.txt')
i = 1
s = '1234'
d = {'abc':1}
t = (1,2,344)
m = {1,2,34,}

print(isinstance(i, Iterable))  # 判斷整型是否為可迭代物件
print(isinstance(s, Iterable))  # 判斷字串物件是否為可迭代物件  
print(isinstance(d, Iterable))  # 判斷字典物件是否為可迭代物件
print(isinstance(t, Iterable))  # 判斷元組物件是否為可迭代物件
print(isinstance(m, Iterable))  # 判斷集合物件是否為可迭代物件
print(isinstance(f, Iterable))  # 判斷檔案物件是否為可迭代物件

########輸出結果#########
False
True
True
True
True
True

  由上面得出,除了整型之外,python內的基本資料型別都是可迭代物件,包括檔案物件。那麼,python內部是如何知道一個物件是否為可迭代物件呢?答案是,在每一種資料型別物件中,都會有有一個__iter__()方法,正是因為這個方法,才使得這些基本資料型別變為可迭代。 

  如果不信,我們可以來看看下面程式碼片段:

 

f = open('a.txt')
i = 1
s = '1234'
d = {'abc':1}
t = (1,2,344)
m = {1,2,34,}


# hasattr(obj,string) 判斷物件中是否存在string方法
print(hasattr(i, '__iter__'))
print(hasattr(s, '__iter__'))
print(hasattr(d, '__iter__'))
print(hasattr(t, '__iter__'))
print(hasattr(m, '__iter__'))
print(hasattr(f, '__iter__'))

#########輸出結果#######
C:\Python35\python3.exe D:/CODE_FILE/python/day21/迭代器.py
False
True
True
True
True
True

  如果大家還是不信,可以繼續來測試。我們自己來寫一個類,看看有__iter__()方法和沒有此方法的區別。

 

# 沒有__iter__()方法
class Animal:
    def __init__(self):
        pass

cat = Animal()

print(isinstance(cat, Iterable))

######輸出結果##########
False

# 有__iter__()方法
class Animal:
    def __init__(self):
        pass

    def __iter__(self):
        pass

cat = Animal()

print(isinstance(cat, Iterable))

######輸出結果##########
True

  從上面,實驗結果可以看出一個物件是否可迭代,關鍵看這個物件是否有__iter__()方法。

3.迭代器

  在介紹迭代器之前,我們先來了解一下容器這個概念。

  容器是一種把多個元素組織在一起的資料結構,容器中的元素可以逐個地迭代獲取。簡單來說,就好比一個盒子,我們可以往裡面存放資料,也可以從裡面一個一個地取出資料。

  在python中,屬於容器型別地有:list,dict,set,str,tuple.....。容器僅僅只是用來存放資料的,我們平常看到的 l = [1,2,3,4]等等,好像我們可以直接從列表這個容器中取出元素,但事實上容器並不提供這種能力,而是可迭代物件賦予了容器這種能力。

  說完了容器,我們在來談談迭代器。迭代器與可迭代物件區別在於:__next__()方法。

  我們可以採用以下方法來驗證一下:

 

from collections import Iterator

f = open('a.txt')
i = 1
s = '1234'
d = {'abc':1}
t = (1, 2, 344)
m = {1, 2, 34, }

print(isinstance(i,Iterator))
print(isinstance(s,Iterator))
print(isinstance(d,Iterator))
print(isinstance(t,Iterator))
print(isinstance(m,Iterator))
print(isinstance(f,Iterator))

########輸出結果##########
False
False
False
False
False
True

  結果顯示:除了檔案物件為迭代器,其餘均不是迭代器

  下面,我們進一步來驗證一下:

 

print(hasattr(i,"__next__"))
print(hasattr(s,"__next__"))
print(hasattr(d,"__next__"))
print(hasattr(t,"__next__"))
print(hasattr(m,"__next__"))
print(hasattr(f,"__next__"))

#######結果###########
False
False
False
False
False
True

  從輸出結果可以表明,迭代器與可迭代物件僅僅就是__next__()方法的有無。

4.for內部機制剖析

  先來看看一段普通的迭代過程:

l = [1,2,3,4,5]

for i in l:
    print(i)

   根據之前的分析,我們知道 l = [1,2,3,4,5]是一個可迭代物件。而且可迭代物件是不可以直接從其中取到元素。那麼為啥我們還能從列表L中取到元素呢?這一切都是因為for迴圈內部實現。在for迴圈內部,首先L會呼叫__iter__()方法,將列表L變為一個迭代器,然後這個迭代器再呼叫其__next__()方法,返回取到的第一個值,這個元素就被賦值給了i,接著就列印輸出了。

  下面,我們通過一系列的實驗來證明上述所說的。

 

l = [1,2,3,4,5,6]

item = l.__iter__()  # 將l變為迭代器
print(item.__next__())  # 迭代器呼叫next方法,並且返回取出的元素
print(item.__next__())
print(item.__next__())
print(item.__next__())
print(item.__next__())
print(item.__next__())
print(item.__next__())  # 報錯
#######輸出結果############# 
1 2 3 4 5 6

######上面為什麼報錯呢??##########
#當呼叫了最後一個next方法,沒有下一個元素可取
#就會報錯StopIteration異常錯誤。你可能會想會
#為什麼for迴圈沒有報錯?答案很簡單,因為for循
#環內部幫我們捕捉到了這個異常,一旦捕捉到異常
#說明,迭代應該結束了!
###########################

  上述實驗,與我上面說明的一致。  

  下面,我們可以while迴圈來模擬for迴圈,輸出列表中的元素。

l = [1,2,3,4,5]

item = l.__iter__()  # 生成一個迭代器

while True:
    try:
        i = item.__next__()
        print(i)
    except StopIteration:  # 捕獲異常,如果有異常,說明應該停止迭代
        break

  由上分析,我們可以總結出:當我們試圖用for迴圈來迭代一個可迭代物件時候,for迴圈在內部進行了兩步操作:第一,將可迭代物件S變為迭代器M;第二,迭代器M呼叫__next__()方法,並且返回其取出的元素給變數i。

  

  你可能看見過這種寫法,for i in iter(M):xxx ,其實這一步操作和我們上面沒什麼區別。iter()函式,就是將一個可迭代物件M變為迭代器也就是M呼叫__iter__()方法,然後內部在呼叫__next__()方法。也就是說,

M = [1,2,3,4,5]

for i in iter(M):  # 等價於 M.__iter()__   人為顯示呼叫
    print(i)

for i in M:  # 直譯器隱式呼叫
    print(i)

#################
#
#上面輸出的結果完全一樣
#
#################

  還有next(M)等價於M.__next__。  

  迭代器優點:

    1.節約記憶體

    2.不依賴索引取值

    3.實現惰性計算(什麼時候需要,在取值出來計算)

5.生成器(本質就是迭代器)

  什麼是生成器?可以理解為一種資料型別,這種資料型別自動實現了迭代器協議(其他的資料型別需要呼叫自己內建的__iter__方法)。

  按照我們之前所說的,迭代器必須滿足兩個條件:既有__iter__(),又有__next__()方法。那麼生成器是否也有這兩個方法呢?答案是,YES。具體來通過以下程式碼來看看。

 

def func():
    print("one------------->")
    yield 1
    print("two------------->")
    yield 2
    print("three----------->")
    yield 3
    print("four------------>")
    yield 4

print(hasattr(func(),'__next__'))
print(hasattr(func(),'__iter__'))

#########輸出結果###########
True
True

  實驗表明,生成器就是迭代器。

  Python有兩種不同的方式提供生成器:

    1.生成器函式(函式內部有yield關鍵字):常規函式定義,但是,使用yield語句而不是return語句返回結果。yield語句一次返回一個結果,在每個結果中間,掛起函式的狀態,以便下次重它離開的地方繼續執行

    2.生成器表示式:類似於列表推導,但是,生成器返回按需產生結果的一個物件,而不是一次構建一個結果列表

  既然生成器就是迭代器,那麼我們是不是也可以通過for迴圈來遍歷出生成器中的內容呢?看下面程式碼.

 

def func():
    print("one------------->")
    yield 1
    print("two------------->")
    yield 2
    print("three----------->")
    yield 3
    print("four------------>")
    yield 4

for i in func():
    print(i)

#########輸出結果########
one------------->
1
two------------->
2
three----------->
3
four------------>
4

  很顯然,生成器也可以通過for迴圈來遍歷出其中的內容。

  下面我們來看看生成器函式執行流程:

 

def func():
    print("one------------->")
    yield 1
    print("two------------->")
    yield 2
    print("three----------->")
    yield 3
    print("four------------>")
    yield 4


g = func()  #  生成器 == 迭代器
print(g.__next__())
print(g.__next__())
print(g.__next__())
print(g.__next__())

  每次呼叫g.__next__()就回去函式內部找yield關鍵字,如果找得到就輸出yield後面的值並且返回;如果沒有找到,就會報出異常。上述程式碼中如果在呼叫g.__next__()就會報錯。

  Python使用生成器對延遲操作提供了支援。所謂延遲操作,是指在需要的時候才產生結果,而不是立即產生結果。這也是生成器的主要好處。

1

例項:生成器模擬Linux下tail -f a.txt | grep 'error' | grep '404'

 

import time


def tail(filepath):
    with open(filepath, encoding='utf-8') as f:
        f.seek(0, 2)  # 停到末尾開頭 1從當前位置  2從檔案末尾
        while True:
            line = f.readline()
            if line:  # 如果有內容讀出
                #print(line,end='')
                yield line  # 遍歷時停在此行,並且將其返回值傳遞出去
            else:
                time.sleep(0.5)  # 如果檔案為空,休眠 等待輸入


def grep(lines, patterns):  # lines為生成器型別
    for line in lines:  # 遍歷生成器
        if patterns in line:
            yield line


g = grep(tail('a.txt'), 'error')  # 動態跟蹤檔案新新增的內容,並且過濾出有patterns的行
g1 = grep(g,'404')  # g1為生成器
for i in g1: # 通過for迴圈來隱式呼叫__next__()方法
    print(i)

  生成器小結:

    1.是可迭代物件

    2.實現了延遲計算,省記憶體啊

    3.生成器本質和其他的資料型別一樣,都是實現了迭代器協議,只不過生成器附加了一個延遲計算省記憶體的好處,其餘的可迭代物件可沒有這點好處!

6.可迭代物件、迭代器、生成器關係總結