Python基礎之迭代器與生成器

迭代器

從【for】說起

在 Python 中,我們學過的數據類型有可以使用 for 循環的,例如:str,list,tuple,dict,set 等; 也有不可以使用 for 循環的,例如:bool, int 等。

我們將一個 int 類型的數據放入 for 循環中:

for i in 123:
    print(i)

會得到這樣一行報錯:
報錯啦!!!
翻譯成中文就是int對象是不可以迭代的

那麼是不是 for 循環只能用於可迭代對象呢?

是的!


可迭代

從 for 循環我們談到了 可迭代 的概念。因爲 123 是不可迭代的,所以它不能被 for 循環遍歷。 而 字符串,列表,元祖,字典,集合 都是可以被遍歷的,那麼這些數據類型都是可迭代的。

我們可以藉助 isinstance() 函數來驗證我們的猜想。

isinstance(object, classinfo)
· object ——實例對象。
· classinfo —— 可以是直接或間接類名、基本類型或者由它們組成的元組。
如果參數object是classinfo的實例,或者object是classinfo類的子類的一個實例, 返回True。如果object不是一個給定類型的的對象, 則返回結果總是False。

from collections import Iterable
print(isinstance(True, Iterable))  # False
print(isinstance(123, Iterable))  # False
print(isinstance('123', Iterable))  # True
print(isinstance([1, 2, 3], Iterable))  # True
print(isinstance((1, 2, 3), Iterable))  # True
print(isinstance({1: 'A', 2: 'B', 3: 'C'}, Iterable))  # True
print(isinstance({1, 2, 3}, Iterable))  # True

結合上面 for 循環的取值現象,我們可以認定,可迭代就是可以將數據集中的元素一個挨一個的取出來


可迭代協議

從上面知道能被 for 循環的數據類型是 “可迭代的”,那麼 for 循環是怎麼判定數據類型是否是可迭代的呢?

在創造一個數據類型的時候,要想這個數據類型能被 for 一個一個的取值,那麼就需要滿足使用 for 的某種要求。這種要求我們可以看做一種 “協議”。 可以滿足被迭代要求的協議稱作 “可迭代協議”

可迭代協議數據對象內部實現了一個__iter__的方法

下面我們來驗證上面的說法。 Python 中的 dir() 函數返回一個包含 參數對象的屬性、方法的列表。

num_dir = dir(123)
str_dir = dir('123')
list_dir = dir([1, 2, 3])
tuple_dir = dir((1, 2, 3))
dict_dir = dir({1: 'A', 2: 'B', 3: 'C'})
set_dir = dir({1, 2, 3})

print('Yes') if '__iter__' in num_dir else print('No')  # No
print('Yes') if '__iter__' in str_dir else print('No')  # Yes
print('Yes') if '__iter__' in list_dir else print('No')  # Yes
print('Yes') if '__iter__' in tuple_dir else print('No')  # Yes
print('Yes') if '__iter__' in dict_dir else print('No')  # Yes
print('Yes') if '__iter__' in set_dir else print('No')  # Yes

在上面我們可以看到: 所有能被 for 循環的數據類型內部都有一個__iter__方法。

那麼我們來調用這個雙下iter方法,看看會出現什麼結果。

list_obj = [1, 2, 3]
print(list_obj.__iter__())
# <list_iterator object at 0x00000140F0CB4978>

對於 list 類型的對象執行了__iter__方法後, 產生了一個 list_iterator。 iterator : 迭代器。


迭代器協議

剛剛我們得到了一個 list_iterator,就是列表迭代器。 那麼是什麼是迭代器呢? 列表迭代器又和列表有什麼區別呢?

print(dir([1, 2, 3]))
print(dir([1, 2, 3].__iter__()))

分別打印 列表 和 列表迭代器 中包含的方法。 爲了更好地看出其中的區別,可以:

print(set(dir([1, 2, 3].__iter__())) - set(dir([1, 2, 3])))

得到: {'__next__', '__setstate__', '__length_hint__'}

list_iter = [1, 2, 3, 6324, '國機二院徐嘉浩', 'gkd'].__iter__()
# 獲取迭代器的長度
print(list_iter.__length_hint__())  # 6
# 指定開始迭代的位置
print(list_iter.__setstate__(3))  # None
# 按照索引順序從迭代器中取值,一次取一個
print(list_iter.__next__())  # 6324
print(list_iter.__next__())  # 國機二院徐嘉浩
print(list_iter.__next__())  # gkd
print(list_iter.__next__())  # StopIteration 拋出異常

迭代器中的三個方法就實現了 for 循環的效果。__ next __ 一個一個取值的過程就是 for 的取值過程,但是不同與 for 的是,在無法從迭代器中取到值的時候,__ next __()方法會拋出一個異常StopIteration。

迭代器遵循迭代器協議必須擁有__iter__和__next__方法。

好,下面我們用異常處理機制和 while 來模擬 for 的功能。

list_iter = [1, 2, 3, 6324, '國機二院徐嘉浩', 'gkd'].__iter__()

while True:
    try:
        item = list_iter.__next__()
        print(item)
    except StopIteration:
        break

range()究竟是什麼

借用上面的知識,一起來判定一下 range() 究竟是可迭代對象還是迭代器?

from collections import Iterable

print(isinstance(range(10), Iterable))  # True
print('__iter__' in dir(range(10)))  # True
print('__next__' in dir(range(10)))  # False

所以 range() 是一個可迭代對象,並不是迭代器



生成器

【for】存在的意義

從上面的討論中我們知道 for 是基於迭代器協議,調用__iter__()方法將數據容器轉化爲迭代器,並用__next__()一個接着一個的取出數據來實現對數據容器的循環遍歷的。

但是我們好像使用 while 就可以輕鬆實現 for 的功能。例如對於一個列表:

li = [1, 2, 3, 6324, '國機二院徐嘉浩', 'gkd']
index = 0
while index < len(li):
    print(li[index])
    index += 1

是不是很輕鬆? 那 for 還有存在的意義嗎?

我們在 while 中實現的循環遍歷是基於列表的下標的。 對於序列類型的數據容器我們都可以用 while 去實現遍歷。 那麼 像 字典,集合, 文件對象這類非序列類型數據容器呢?
for 存在的意義就是對於非序列類型的數據容器也可以實現遍歷。


認識生成器

爲什麼要使用迭代器呢? 除了可以實現對非序列類型的數據容器的取值,還有什麼好處呢?
迭代器是一個接一個取值的,可以做到需要多少就取多少。好處就是可以節省內存
在實際開發過程中,內存問題是我們必須要考慮的問題。 很多時候爲了節省內存,我們需要自己寫能實現迭代器功能的東西,這種東西就叫做生成器

生成器Generator
  本質:迭代器(所以自帶了__iter__方法和__next__方法,不需要我們去實現)
  特點:惰性運算,開發者自定義

Python 中提供的兩種生成器:

  1. 生成器函數:常規函數定義。但是,使用yield語句而不是return語句返回結果。yield語句一次返回一個結果,在每個結果中間,掛起函數的狀態,以便下次重它離開的地方繼續執行。
  2. 生成器表達式:類似於列表推導,但是,生成器返回按需產生結果的一個對象,而不是一次構建一個結果列表。

生成器函數

初次見面,生成器函數你好!

包含關鍵字 yield 的函數就是生成器函數。yield 也可以像 return 一樣爲我們從函數中返回值。 但不同於 return 的是, 函數執行到 yield 這一步時不會終止程序,而是將程序掛起,在下一次調用時會緊接着上次斷開的位置繼續執行。 所以一個 生成器函數中可以存在多次 yield 函數。 直接調用生成器函數不會返回一個具體的值,而是得到一個迭代器。 正是這個迭代器一次一次的獲取值,才推動生成器函數的執行。

import time
from collections import Iterable


def generator_fuc():
    a = 1
    print('here is a')
    yield a
    print('o'*30)  # 停頓3秒後才執行這句輸出,說明第一次調用g1時,在yield a執行結束後,程序就掛起了
    b = 2
    print('here is b')
    yield b


g1 = generator_fuc()  # 調用生成器函數
print(g1)  # 打印g1,得到<generator object generator_fuc at 0x000001F8076351A8>,說明g1是一個生成器

# 驗證生成器g1就是個迭代器
print(isinstance(g1, Iterable))
print('__iter__' in dir(g1))
print('__next__' in dir(g1))


print('\n' + '-'*30 + '\n')  # 我是分割線
print(next(g1))
time.sleep(3)
print(next(g1))

那麼生成器有什麼好處呢? 這樣的取值方式比一次性取完保存到內存中有哪些優點呢?

顯而易見,生成器可以避免一次性在內存中產生太多的數據。 基於生成器的這種特性,可以做到即用即取

例如,公司需要生產一批零部件用於組裝產品。 計劃組裝產品10萬個,那麼也就需要10萬個零部件。 在實際的生產過程中,不可能說公司一次性生產完10萬個零部件,纔開始組裝產品。 這樣做可能早就錯過了產品的搶佔市場的時機或者是最佳銷售期。 所以要按照批次生產零部件,組裝產品。

假設一個批次生產1000個零部件:

def produce():
    """生產零部件"""
    for i in range(100000):
        yield '這是生產的第%s件零部件' % i


# 檢測是否能順利生產
product_g = produce()
print(product_g.__next__())
print(product_g.__next__())
print(product_g.__next__())

# 生產第一個批次
product_g1 = produce()
num = 0
for item in product_g1:
    print(item)
    num += 1
    if num == 1000:
        break

生成器監聽文件輸入

def detector(filename):
    f = open(filename, mode='r', encoding='utf-8')
    f.seek(0, 2)  # 將光標移動到文件末尾
    while True:
        content = f.readline()
        if content.strip():
            yield content.strip()
            
detector_g = detector('A.txt')
for line in detector_g:
    print(line)

利用生成器監測文件中的輸入內容。運行生成器程序時,在文件中輸入的內容會輸出到Python輸出的控制檯中。
注意: 因爲不同電腦本地運行的區別,監測不一定是及時的,可能會出現較長時間的停滯反應。


send的用法

關於 send 怎麼用,我們直接來看一段代碼

def generator():
    print('沖沖衝')
    content = yield 4396  # 注意這裏!! yield 4396 不僅是一個表達式,也變成了一個值,並賦給了content
    print('content =', content)  # content == 7777777
    print('沖沖衝')
    yield 2800

g = generator()
ret1 = g.__next__()
print('ret1 =', ret1)
ret2 = g.send(7777777)
print('ret2 =', ret2)

運行一下,我們發現 content 的值是 7777777。

仔細閱讀這段代碼,我們發現了和前面寫的生成器函數不同的地方。 yield 不僅僅是用來爲生成器返回值了,它也可以爲生成器函數中的變量賦值。send() 和__next__()在接收生成器中的值的作用上並沒有什麼差別,不同的是 send() 在接收當前 yield 返回的值之外,還可以從外部往上一個 yield 的位置傳遞一個值。

所以在使用 send() 的時候,我們需要注意

1. 第一次調用生成器的時候,必須使用__next__(),或者是send(None)
2. 生成器函數中的最後一個 yield 是不能接收到外部的值的。

程序實例: 實時計算平均數。 計算當前從鍵盤輸入的所有數字的平均值。

def average_num():
    total = 0.0
    cnt = 0
    avg = None
    while True:
        term = yield avg
        total += term
        cnt += 1
        avg = total/cnt


g_avg = average_num()
next(g_avg)
while True:
    A = input()
    if A.isdigit():  # 如果A是數字組成的
        print(g_avg.send(int(A)))
    if A == 'Break':
        break

帶預激協程的生成器

預激協程就是用來預先激活生成器的協助程序。在一些場景中,使用生成器的時候側重 send() 的功能,那麼第一次的yield返回值就不重要了。我們每一次碰到這樣的場景都需要先寫一個 next( g ) 來激活生成器。 爲了更方便,更規範(代碼儘量模塊化);我們藉助裝飾器來幫我們完成這個步驟。

以上面的 “實時計算平均數” 的程序爲例:

from functools import wraps

def wrapper(fun):
    @wraps(fun)
    def inner(*args, **kwargs):
        g = fun(*args, **kwargs)
        next(g)
        return g
    return inner

@wrapper
def average_num():
    total = 0.0
    cnt = 0
    avg = None
    while True:
        term = yield avg
        total += term
        cnt += 1
        avg = total/cnt

g_avg = average_num()
# next(g_avg)  裝飾器函數已經提前完成了這一步 
while True:
    A = input()
    if A.isdigit():  # 如果A是數字組成的
        print(g_avg.send(int(A)))
    if A == 'Break':
        break

yield from

yield from 後面接可迭代對象。 可以是普通的可迭代對象,迭代器;也可以是一個生成器。
yield from可以把可迭代對象裏的每個元素一個一個的 yield 出來,對比 yield 來說代碼更加簡潔,結構更加清晰。

def gen1():
    for s in 'abc':
        yield s
    for i in range(3):
        yield i

print(list(gen1()))


def gen2():
    yield from 'abc'
    yield from range(3)

print(list(gen2()))


生成器表達式

前面我們學過了 列表推導式, 可以用一句話表示出列表的構造。 生成器表達式也具有這個功能。也可以用一句話表示出生成器。而且它與列表推導式的區別僅僅是將 [] 換成了 () 。但是生成器表達式更加的節省內存。

例如:

list_even_num = [i for i in range(20) if i % 2 == 0]
print(list_even_num)  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

gen_even_num = (i for i in range(20) if i % 2 == 0)
print(gen_even_num)  # <generator object <genexpr> at 0x0000025F37E61D00>
print(next(gen_even_num))  # 0
for i in gen_even_num:
    print(i, ' ', end='')  # 2  4  6  8  10  12  14  16  18  

Python不但使用迭代器協議,讓for循環變得更加通用。大部分內置函數,也是使用迭代器協議訪問對象的。例如,sum函數是Python的內置函數,該函數使用迭代器協議訪問對象,而生成器實現了迭代器協議.。

所以,我們可以直接這樣計算一系列值的和:

sum(i for i in range(20) if i % 2 == 0)

而不用多此一舉地構造一個列表:

sum([i for i in range(20) if i % 2 == 0])

文章參考於http://www.cnblogs.com/Eva-J/articles/7213953.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章