1. 程式人生 > >python迭代器、生成器和yield語句

python迭代器、生成器和yield語句

一、迭代器(iterator)

迭代器:是一個實現了迭代器協議的物件,Python中的迭代器協議就是有next方法的物件會前進到下一結果,而在一系列結果的末尾是,則會引發StopIteration。任何這類的物件在Python中都可以用for迴圈或其他遍歷工具迭代,迭代工具內部會在每次迭代時呼叫next方法,並且捕捉StopIteration異常來確定何時離開。

迭代器物件要求支援迭代器協議的物件,在Python中,支援迭代器協議就是實現物件的__iter__()和next()方法。其中__iter__()方法返回迭代器物件本身;next()方法返回容器的下一個元素,在結尾時引發StopIteration異常。當我們使用for語句的時候,for語句就會自動的通過__iter__()方法來獲得迭代器物件,並且通過next()方法來獲取下一個元素。在Python中,for迴圈可以用於Python中的任何型別,包括列表、元祖等等,實際上,for迴圈可用於任何“可迭代物件”,這其實就是迭代器。

使用迭代器一個顯而易見的好處就是:每次只從物件中讀取一條資料,不會造成記憶體的過大開銷。
本質上說迭代器是個物件,但是這個物件有個特殊的方法next()(在python3中使用__next__()代替了next方法)。當使用for迴圈來遍歷整個物件時候,就會自動呼叫此物件的__next__()方法並獲取下一個item。當所有的item全部取出後就會丟擲一個StopIteration異常,這並不是錯誤的發生,而是告訴外部呼叫者迭代完成了,外部的呼叫者嘗試去捕獲這個異常去做進一步的處理。
不過迭代器是有限制的,例如
    不能向後移動
    不能回到開始
    也無法複製一個迭代器。
    因此要再次進行迭代只能重新生成一個新的迭代器物件。

舉個栗子

比如要逐行讀取一個檔案的內容,利用readlines()方法,我們可以這麼寫:
for line in open("test.txt").readlines():
    print line
這樣雖然可以工作,但不是最好的方法。因為他實際上是把檔案一次載入到記憶體中,然後逐行列印。當檔案很大時,這個方法的記憶體開銷就很大了。

利用file的迭代器,我們可以這樣寫:
for line in open("test.txt"):   #use file iterators
    print line
這是最簡單也是執行速度最快的寫法,他並沒顯式的讀取檔案,而是利用迭代器每次讀取下一行。

迭代器和可迭代物件

自定義迭代器

示例1

普通的迭代器只能迭代一輪,一輪之後重複呼叫是無效的。解決這種問題的方法是,你可以定義一個可迭代的容器類:

class LoopIter(object):
    def __init__(self, data):
        self.data = data
    # 必須在 __iter__ 中 yield 結果
    def __iter__(self):
        for index, letter in enumerate(self.data):
            if letter == 'a':
                yield index

這樣的話,將類的例項迭代重複多少次都沒問題:

for _ in indexs:
    print(_)
# loop 1
print('loop 2')
for _ in indexs:
    print(_)

但要注意的是,僅僅是實現__iter__方法的迭代器,只能通過for迴圈來迭代;想要通過next方法迭代的話則需要使用iter方法next(indexs)# TypeError: 'LoopIter' object is not an iteratoriter_indexs=iter(indexs)next(iter_indexs)# 8
示例2

下面例子中實現了一個MyRange的型別,這個型別中實現了__iter__()方法,通過這個方法返回物件本身作為迭代器物件;同時,實現了next()方法用來獲取容器中的下一個元素,當沒有可訪問元素後,就丟擲StopIteration異常。

class MyRange(object):
    def __init__(self, n):
        self.idx = 0
        self.n = n
 
    def __iter__(self):
        return self
 
    def next(self):
        if self.idx < self.n:
            val = self.idx
            self.idx += 1
            return val
        else:
            raise StopIteration()

這個自定義型別跟內建函式xrange很類似。

在上面的例子中,myRange這個物件就是一個可迭代物件,同時它本身也是一個迭代器物件。

對於一個可迭代物件,如果它本身又是一個迭代器物件,就沒有辦法支援多次迭代。(iter返回的是本身self)

為了解決上面的問題,可以分別定義可迭代型別物件和迭代器型別物件;然後可迭代型別物件的__iter__()方法可以獲得一個迭代器型別的物件。

分開定義的好處在於, 當對可迭代物件使用iter()轉變時,返回一個新的迭代器物件, 這時不受先前產生的相應迭代器物件影響。

看下面的實現:

class Zrange:
    def __init__(self, n):
        self.n = n
 
    def __iter__(self):
        return ZrangeIterator(self.n)
 
class ZrangeIterator:
    def __init__(self, n):
        self.i = 0
        self.n = n
 
    def __iter__(self):
        return self
 
    def next(self):
        if self.i < self.n:
            i = self.i
            self.i += 1
            return i
        else:
            raise StopIteration()    
 
zrange = Zrange(3)
print zrange is iter(zrange)        
 
print [i for i in zrange]
print [i for i in zrange]
程式碼的執行結果為:

其實,通過下面程式碼可以看出,list型別也是按照上面的方式,list本身是一個可迭代物件,通過iter()方法可以獲得list的迭代器物件:

zrange = Zrange(3)
print zrange is iter(zrange)   
#>>> True  
print [i for i in zrange]
#>>>[1,2,3]
print [i for i in zrange]
#>>>[1,2,3]

# 若不區分可迭代物件和迭代器, 即這裡列表生成式中使用ZrangeIterator的話, 第二次呼叫時迭代器已被迭代完,第二次會為空集.
zzrange=ZrangeIterator(3);
print [i for i in zzrange]
#>>>[1,2,3]
print [i for i in zzrange]
#>>>[]
皮皮Blog

二、生成器(constructor)

在Python中,使用生成器可以很方便的支援迭代器協議。生成器通過生成器函式產生,生成器函式可以通過常規的def語句來定義,但是不用return返回,而是用yield一次返回一個結果,在每個結果之間掛起和繼續它們的狀態,來自動實現迭代協議。也就是說,yield是一個語法糖,內部實現支援了迭代器協議,同時yield內部是一個狀態機,維護著掛起和繼續的狀態。

生成器函式在Python中與迭代器協議的概念聯絡在一起。簡而言之,包含yield語句的函式會被特地編譯成生成器。當函式被呼叫時,他們返回一個生成器物件,這個物件支援迭代器介面。

函式也許會有個return語句,但它的作用是用來yield產生值的。不像一般的函式會生成值後退出,生成器函式在生成值後會自動掛起並暫停他們的執行和狀態,他的本地變數將儲存狀態資訊,這些資訊在函式恢復時將再度有效。

生成器的使用

例子中定義了一個生成器函式,函式返回一個生成器物件,然後就可以通過for語句進行迭代訪問了。

其實,生成器函式返回生成器的迭代器。 “生成器的迭代器”這個術語通常被稱作”生成器”。要注意的是生成器就是一類特殊的迭代器。作為一個迭代器,生成器必須要定義一些方法,其中一個就是next()。如同迭代器一樣,我們可以使用next()函式來獲取下一個值。

Note: 這裡的函式Zrange必須要生成例項,不能使用Zrange(3).__next__()形式呼叫,這相當於每次新生成了一個Zrange物件,這樣裡面的迴圈每次呼叫都只會迴圈一次!

生成器執行流程

生成器是怎麼工作的?從上面的例子也可以看到,生成器函式跟普通的函式是有很大差別的。加入一些列印資訊,進一步看看生成器的執行流程:

通過結果可以看到:

  • 當呼叫生成器函式的時候,函式只是返回了一個生成器物件,並沒有 執行。
  • 當next()方法第一次被呼叫的時候,生成器函式才開始執行,執行到yield語句處停止
    • next()方法的返回值就是yield語句處的引數(yielded value)
  • 當繼續呼叫next()方法的時候,函式將接著上一次停止的yield語句處繼續執行,併到下一個yield處停止;如果後面沒有yield就丟擲StopIteration異常
皮皮Blog

Generator expressions生成器表示式

為什麼要有生成器表示式

列表解析可能出現的問題:List comprehensions have one possible problem, and that is they build the list in memory right away. If your dealing with big data sets, that can be a big problem, but even with small lists, it is still extra overhead that might not be needed. 

If you are only going to loop over the results once it has no gain in building this list. So if you can give up being able to index into the result, and do other list operations, you can use a generator expression, which uses very similar syntax, but creates a lazy object, that computes nothing until you ask for a value.

列表推導也可能會有一些負面效應,那就是整個列表必須一次性加載於記憶體之中,這對上面舉的例子而言不是問題,甚至擴大若干倍之後也都不是問題。但是總會達到極限,記憶體總會被用完。
針對上面的問題,生成器(Generator)能夠很好的解決。生成器表示式不會一次將整個列表載入到記憶體之中,而是生成一個生成器物件(Generator objector),所以一次只加載一個列表元素。

除非特殊的原因,應該經常在程式碼中使用生成器表示式。但除非是面對非常大的列表,否則是不會看出明顯區別的。

# generator expression for the square of all the numbers squares = (num * num for num in numbers) # where you would likely get a memory problem otherwise  with open('/some/number/file', 'r') as f: squares = (int(num) * int(num) for num in f) # do something with these numbers

生成器表示式是在python2.4中引入的,當序列過長, 而每次只需要獲取一個元素時,應當考慮使用生成器表示式而不是列表解析。和列表解析一樣,只不過生成器表示式是被()括起來的:(expr foriter_var initerable ifcond_expr)

生成器表示式並不是建立一個列表, 而是返回一個生成器,這個生成器在每次計算出一個條目後,把這個條目”產生”(yield)出來。 生成器表示式使用了”惰性計算”(lazy evaluation),只有在檢索時才被賦值(evaluated),所以在列表比較長的情況下使用記憶體上更有效。

使用生成器

    考慮使用生成器來改寫直接返回列表的函式
# 定義一個函式,其作用是檢測字串裡所有 a 的索引位置,最終返回所有 index 組成的陣列
def get_a_indexs(string):
    result = []
    for index, letter in enumerate(string):
        if letter == 'a':
            result.append(index)
    return result
用列表方法有幾個小問題:
    每次獲取到符合條件的結果,都要呼叫append方法。但實際上我們的關注點根本不在這個方法,它只是我們達成目的的手段,實際上只需要index就好了
    返回的result可以繼續優化
    資料都存在result裡面,如果資料量很大的話,會比較佔用記憶體
因此,使用生成器generator會更好。生成器是使用yield表示式的函式,呼叫生成器時,它不會真的執行,而是返回一個迭代器,每次在迭代器上呼叫內建的next函式時,迭代器會把生成器推進到下一個yield表示式:
def get_a_indexs(string):
    for index, letter in enumerate(string):
        if letter == 'a':
            yield index
獲取到一個生成器以後,可以正常的遍歷它:
string = 'this is a test to find a\' index'
indexs = get_a_indexs(string)
for i in indexs:
    print(i)
# 或者這樣
try:
    while True:
        print(next(indexs))
except StopIteration:
    print('finish!')
# 生成器在獲取完之後如果繼續通過 next() 取值,則會觸發 StopIteration 錯誤, 但通過 for 迴圈遍歷時會自動捕獲到這個錯誤
如果你還是需要一個列表,那麼可以將函式的呼叫結果作為引數,再呼叫list方法
results = get_a_indexs('this is a test to check a')
results_list = list(results)

舉個栗子

從這個例子中可以看到,生成器表示式產生的生成器,它自身是一個可迭代物件,同時也是迭代器本身。

遞迴生成器

生成器可以向函式一樣進行遞迴使用的,下面看一個簡單的例子,對一個序列進行全排列:

def permutations(li):
    if len(li) == 0:
        yield li
    else:
        for i in range(len(li)):
            li[0], li[i] = li[i], li[0]
            for item in permutations(li[1:]):
                yield [li[0]] + item
 
for item in permutations(range(3)):
    print item

程式碼的結果為:

生成器的send()和close()方法

send(value):

從前面瞭解到,next()方法可以恢復生成器狀態並繼續執行,其實send()是除next()外另一個恢復生成器的方法。

Python 2.5中,yield語句變成了yield表示式,也就是說yield可以有一個值,而這個值就是send()方法的引數,所以send(None)和next()是等效的。同樣,next()和send()的返回值都是yield語句處的引數(yielded value)

關於send()方法需要注意的是:呼叫send傳入非None值前,生成器必須處於掛起狀態,否則將丟擲異常。也就是說,第一次呼叫時,要使用next()語句或send(None),因為沒有yield語句來接收這個值。

close():

這個方法用於關閉生成器,對關閉的生成器後再次呼叫next或send將丟擲StopIteration異常。

下面看看這兩個方法的使用:

生成器語句yield

1. 包含yield的函式

假如你看到某個函式包含了yield,這意味著這個函式已經是一個Generator,它的執行會和其他普通的函式有很多不同。
def h():
    print('To be brave')
    yield 5

h()Note:呼叫h()之後,print 語句並沒有執行!

2. yield是一個表示式

Python2.5以前,yield是一個語句,但現在2.5中,yield是一個表示式(Expression),比如:
m = yield 5表示式(yield 5)的返回值將賦值給m,所以,認為 m = 5 是錯誤的。那麼如何獲取(yield 5)的返回值呢?需要用到後面要介紹的send(msg)方法。

3. 透過next()語句看原理

h()被呼叫後並沒有執行,因為它有yield表示式,因此,我們通過next()語句將恢復Generator執行,並直到下一個yield表示式處
def h():
    print('Wen Chuan')
    yield 5
    print 'Fighting!'

c = h()
c.__next__()c.next()呼叫後,h()開始執行,直到遇到yield 5,因此輸出結果:
Wen Chuan
當我們再次呼叫c.next()時,會繼續執行,直到找到下一個yield表示式。由於後面沒有yield了,因此會丟擲異常:
Wen Chuan
Fighting!
Traceback (most recent call last):
  File "/home/evergreen/Codes/yidld.py", line 11, in <module>
    c.__next__()
StopIteration

4. send(msg) 與 next()

再來看另外一個非常重要的函式send(msg)。其實next()和send()在一定意義上作用是相似的,區別是send()可以傳遞yield表示式的值進去,而next()不能傳遞特定的值,只能傳遞None進去。因此,c.next() 和 c.send(None) 作用是一樣的。
def h():
    print('Wen Chuan',)
    m = yield 5  # Fighting!    print(m)
    d = yield 12
    print('We are together!')

c = h()
c.__next__()  #相當於c.send(None)c.send('Fighting!')  #(yield 5)表示式被賦予了'Fighting!'輸出的結果為:
Wen Chuan Fighting!
Note:第一次呼叫時,請使用next()語句或是send(None),不能使用send傳送一個非None的值,否則會出錯的,因為沒有yield語句來接收這個值。

5. send(msg) 與 next()的返回值

send(msg) 和 next()是有返回值的,返回的是下一個yield表示式的引數。比如yield 5,則返回 5 。前面例子中,通過for遍歷 Generator,其實是每次都呼叫了Next(),而每次Next()的返回值正是yield的引數,即我們開始認為被壓進去的。
def h():
    print('Wen Chuan',)
    m = yield 5  # Fighting!    print (m)
    d = yield 12
    print('We are together!')

c = h()
m = c.__next__()  #m 獲取了yield 5 的引數值 5d = c.send('Fighting!')  #d 獲取了yield 12 的引數值12print('We will never forget the date', m, '.', d)輸出結果:
Wen Chuan Fighting!
We will never forget the date 5 . 12

6. throw() 與 close()中斷 Generator

中斷Generator是一個非常靈活的技巧,可以通過throw丟擲一個GeneratorExit異常來終止Generator。Close()方法作用是一樣的,其實內部它是呼叫了throw(GeneratorExit)的。我們看:
def close(self):
    try:
        self.throw(GeneratorExit)
    except (GeneratorExit, StopIteration):
        pass
    else:
        raise RuntimeError("generator ignored GeneratorExit")
# Other exceptions are not caught因此,當我們呼叫了close()方法後,再呼叫next()或是send(msg)的話會丟擲一個異常:
Traceback (most recent call last):
  File "/home/evergreen/Codes/yidld.py", line 14, in <module>
    d = c.send('Fighting!')  #d 獲取了yield 12 的引數值12StopIteration

Note:注意python2和python3中迭代器、生成器和yield語句的區別[python2和python3的區別、轉換及共存]

yield使用示例

1 通過兩階列表推導式遍歷目錄

import osdef tree(top):for path, names, fnames in os.walk(top):for fname in fnames:yield os.path.join(path, fname)for name in tree('C:\Users\XXX\Downloads\Test'):print name

2 列表值按需引用

lz發現的一個較好的應用場景是,繪圖時顏色的取用

colors = ['aqua', 'darkorange', 'cornflowerblue']

在函式中yield colors就不用在函式外使用下標呼叫顏色了。

一個更好的代替是

colors = cycle(['aqua', 'darkorange', 'cornflowerblue'])

plt.plot(f_pos, t_pos, color=colors.__next__(), lw=2, label='AUC = %.2f' % auc_area)

3 無窮迴圈器