文章目錄
迭代器
從【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 中提供的兩種生成器:
- 生成器函數:常規函數定義。但是,使用yield語句而不是return語句返回結果。yield語句一次返回一個結果,在每個結果中間,掛起函數的狀態,以便下次重它離開的地方繼續執行。
- 生成器表達式:類似於列表推導,但是,生成器返回按需產生結果的一個對象,而不是一次構建一個結果列表。
生成器函數
初次見面,生成器函數你好!
包含關鍵字 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])