乾貨推薦:看過介紹 Python 迭代器和生成器最易懂、最全面的文章
迭代器與可迭代物件
概念
迭代器:是訪問資料集合內元素的一種方式,一般用來遍歷資料,但是他不能像列表一樣使用下標來獲取資料,也就是說迭代器是不能返回的。
-
Iterator:迭代器物件,必須要實現 next 魔法函式
-
Iterable:可迭代物件,繼承 Iterator,必須要實現 iter 魔法函式
比如:
from collections import Iterable,Iterator a = [1,2,3] print(isinstance(a,Iterator)) print(isinstance(a,Iterable))
返回結果:
False True
在 Pycharm 中使用 alt+b 進去 list 的原始碼中可以看到,在 list 類中有 iter 魔法函式,也就是說只要實現了 iter 魔法函式,那麼這個物件就是可迭代物件。
上面的例子中 a 是一個列表,也是一個可迭代物件,那麼如何才能讓這個 a 變成迭代器呢?使用 iter() 即可。
from collections import Iterable,Iterator a = [1,2,3] a = iter(a) print(isinstance(a,Iterator)) print(isinstance(a,Iterable)) print(next(a)) print('----') for x in a: print(x)
返回結果:
True True 1 ---- 2 3
可以看到現在 a 是可迭代物件又是一個迭代器,說明列表 a 中有 iter 方法,該方法返回的是迭代器,這個時候使用 next 就可以獲取 a 的下一個值,但是要記住迭代器中的數值只能被獲取一次。
梳理迭代器 (Iterator) 與可迭代物件 (Iterable) 的區別:
-
可迭代物件:繼承迭代器物件,可以用 for 迴圈(說明實現了 iter 方法)
-
迭代器物件:可以用 next 獲取下一個值(說明實現了 next 方法),但是每個值只能獲取一次,單純的迭代器沒有實現 iter 魔法函式,所以不能使用 for 迴圈
-
只要可以用作 for 迴圈的都是可迭代物件
-
只要可以用 next() 函式的都是迭代器物件
-
列表,字典,字串是可迭代物件但是不是迭代器物件,如果想變成迭代器物件可以使用 iter() 進行轉換
-
Python 的 for 迴圈本質上是使用 next() 進行不斷呼叫,for 迴圈的是可迭代物件,可迭代物件中有 iter 魔法函式,可迭代物件繼承迭代器物件,迭代器物件中有 next 魔法函式
-
一般由可迭代物件變迭代器物件
大家在學python的時候肯定會遇到很多難題,以及對於新技術的追求,這裡推薦一下我們的Python學習扣qun:784758214,這裡是python學習者聚集地!!同時,自己是一名高階python開發工程師,從基礎的python指令碼到web開發、爬蟲、django、資料探勘等,零基礎到專案實戰的資料都有整理。送給每一位python的小夥伴!每日分享一些學習的方法和需要注意的小細節

可迭代物件
可迭代物件每次使用 for 迴圈一個數組的時候,本質上會從類中嘗試呼叫 iter 魔法函式,如果類中有 iter 魔法函式的話,會優先呼叫iter魔法函式,當然這裡切記 iter 方法必須要返回一個可以迭代的物件,不然就會報錯。
如果沒有定義 iter 魔法函式的話,會建立一個預設的迭代器,該迭代器呼叫 getitem 魔法函式,如果你沒有定義 iter 和 getitem 兩個魔法函式的話,該型別就不是可迭代物件,就會報錯。
比如:
class s: def __init__(self,x): self.x = x def __iter__(self): return iter(self.x) # 這裡必須要返回一個可以迭代的物件 # def __getitem__(self, item): #return self.x[item] # iter和getitem其中必須要實現一個 a = s('123') # 這裡的a就是可迭代物件 # 這裡不能呼叫next(a)方法,因為沒有定義 for x in a: print(x)
這裡把註釋符去掉返回結果也是一樣的,返回結果:
迭代器物件
一開始提起,iter 搭配 Iterable 做可迭代物件,next 搭配 Iterator 做迭代器。next() 接受一個迭代器物件,作用是獲取迭代器物件的下一個值,迭代器是用來做迭代的,只會在需要的時候產生資料。
和可迭代物件不同,可迭代物件一開始是把所有的列表放在一個變數中,然後用 getitem 方法不斷的返回數值,getitem 中的 item 就是索引值。
但是 next 方法並沒有索引值,所以需要自己維護一個索引值,方便獲取下一個變數的位置。
class s: def __init__(self,x): self.x = x # 獲取傳入的物件 self.index = 0 # 維護索引值 def __next__(self): try: result = self.x[self.index] # 獲取傳入物件的值 except IndexError: # 如果索引值錯誤 raise StopIteration # 丟擲停止迭代 self.index += 1 # 索引值+1,用來獲取傳入物件的下一個值 return result # 返回傳入物件的值 a = s([1,2,3]) print(next(a)) print('----------') for x in a: # 類中並沒有iter或者getitem魔法函式,不能用for迴圈,會報錯 print(x)
返回結果:
Traceback (most recent call last): 1 ---------- File "C:/CODE/Python進階知識/迭代協議/迭代器.py", line 34, in <module> for x in a: TypeError: 's' object is not iterable
上面一個就是完整的迭代器物件,他是根據自身的索引值來獲取傳入物件的下一個值,並不是像可迭代物件直接把傳入物件讀取到記憶體中,所以對於一些很大的檔案讀取的時候,可以一行一行的讀取內容,而不是把檔案的所有內容讀取到記憶體中。
這個類是迭代器物件,那麼如何才能讓他能夠使用 for 迴圈呢?那就讓他變成可迭代物件,只需要在類中加上 iter 魔法函式即可。
class s: def __init__(self,x): self.x = x # 獲取傳入的物件 self.index = 0 # 維護索引值 def __next__(self): try: result = self.x[self.index] # 獲取傳入物件的值 except IndexError: # 如果索引值錯誤 raise StopIteration # 丟擲停止迭代 self.index += 1 # 索引值+1,用來獲取傳入物件的下一個值 return result # 返回傳入物件的值 def __iter__(self): return self a = s([1,2,3]) print(next(a)) print('----------') for x in a: print(x)
返回結果:
1 ---------- 2 3
可以看到這個時候執行成功,但是這個物件還是屬於迭代器物件,因為在 next 獲取下一個值會報錯。
知識整理
根據上面的程式碼提示,得到規律:
-
iter 讓類變成可迭代物件,next 讓類變成迭代器(要維護索引值)。
-
可迭代物件可以用 for 迴圈,迭代器可以用next獲取下一個值。
-
迭代器如果想要變成可迭代物件用 for 迴圈,就要在迭代器內部加上 iter 魔法函式
-
可迭代物件如果想要能用 next 魔法函式,使用自身類中的 iter() 方法即可變成迭代器物件
class s: def __init__(self,x): self.x = x self.index = 0 def __next__(self): try: result = self.x[self.index] except IndexError: raise StopIteration self.index += 1 return result class b: def __init__(self,x): self.x = x def __iter__(self): return s(self.x) a = b([1,2,3]) for x in a: print(x)
返回結果:
這個時候是不能再用 next 方法了,應為類 b 是一個可迭代物件,並非迭代器,這個時候不能用 next 方法,但是可以讓類 b 繼承類 s,這樣就能用 next() 方法獲取下一個值,但是你的類 b 中要存在索引值,不然會報錯,如下程式碼:
class s: def __init__(self,x): self.x = x # 獲取傳入的物件 self.index = 0 # 維護索引值 def __next__(self): try: result = self.x[self.index] # 獲取傳入物件的值 except IndexError: # 如果索引值錯誤 raise StopIteration # 丟擲停止迭代 self.index += 1 # 索引值+1,用來獲取傳入物件的下一個值 return result # 返回傳入物件的值 # def __iter__(self): #return self class b(s): def __init__(self,x): self.x = x self.index = 0 def __iter__(self): return s(self.x) a = b([1,2,3]) print(next(a)) print(next(a))
返回結果:
可以這麼做,但是沒必要,因為這樣違反了設計原則。
迭代器的設計模式
迭代器模式:提供一種方法順序訪問一個聚合物件中的各種元素,而又不暴露該物件的內部
表示。
迭代器的設計模式是一種經典的設計模式,根據迭代器的特性(根據索引值讀取下一個內容,不一次性讀取大量資料到記憶體)不建議將 next 和 iter 都寫在一個類中去實現。
新建一個迭代器,用迭代器維護索引值,返回根據索引值獲取物件的數值,新建另一個可迭代物件,使用 iter 方法方便的迴圈迭代器的返回值。
生成器
生成器:函式中只要有 yield,這個函式就會變成生成器。每次執行到 yield 的時候,函式會暫停,並且儲存當前的執行狀態,返回返回當前的數值,並在下一次執行 next 方法的時候,又從當前位置繼續往下走。
簡單用法
舉個例子:
def gen(): yield 1 # 返回一個物件,這個物件的值是1 def ret(): return 1 # 返回一個數字1 g = gen() r = ret() print(g,r) print(next(g))
返回結果:
<generator object gen at 0x000001487FDA2D58> 1 1
可以看到return是直接返回數值 1,yield 是返回的一個生成器物件,這個物件的值是 1,使用 next(g) 或者 for x in g:print x 都是可以獲取到他的內容的,這個物件是在 python 編譯位元組碼的時候就產生。
def gen(): yield 1 yield 11 yield 111 yield 1111 yield 11111 yield 111111 # 返回一個物件,這個物件內的值是1和11,111... def ret(): return 1 return 3 # 第二個return是無效的 g = gen() r = ret() print(g,r) print(next(g)) for x in g: print(x)
返回結果:
<generator object gen at 0x000002885FE32D58> 1 1 11 111 1111 11111 111111
就像迭代器的特性一樣,獲取過一遍的值是沒法再獲取一次的,並且不是那種一次把所有的結果求出放在記憶體或者說不是一次性讀取所有的內容放在記憶體中。
梳理特性:
-
使用 yield 的函式都是生成器函式
-
可以使用 for 迴圈獲取值,也可以使用 next 獲取生成器函式的值
原理
函式工作原理:函式的呼叫滿足“後進先出”的原則,也就是說,最後被呼叫的函式應該第一個返回,函式的遞迴呼叫就是一個經典的例子。顯然,記憶體中以“後進先出”方式處理資料的棧段是最適合用於實現函式呼叫的載體,在編譯型程式語言中,函式被呼叫後,函式的引數,返回地址,暫存器值等資料會被壓入棧,待函式體執行完畢,將上述資料彈出棧。這也意味著,一個被呼叫的函式一旦執行完畢,它的生命週期就結束了。
Python 直譯器執行的時候,會用 C 語言當中的 PyEval_EvalFramEx 函式建立一個棧幀,所有的棧幀都是分配再堆記憶體上,如果不主動釋放就會一直在裡面。
Python 的堆疊幀是分配在堆記憶體中的,理解這一點非常重要!Python 直譯器是個普通的 C 程式,所以它的堆疊幀就是普通的堆疊。但是它操作的 Python 堆疊幀是在堆上的。除了其他驚喜之外,這意味著 Python 的堆疊幀可以在它的呼叫之外存活。(FIXME: 可以在它呼叫結束後存活),這個就是生成器的核心原理實現。
Python 指令碼都會被 python.exe 編譯成位元組碼的形式,然後 python.exe 再執行這些位元組碼,使用 dis 即可檢視函式物件的位元組碼物件。
import dis # 檢視函式程式位元組碼 a = 'langzi' print(dis.dis(a)) print('-'*20) def sb(admin): print(admin) print(dis.dis(sb))
返回結果:
10 LOAD_NAME0 (langzi) # 載入名字 為langzi 2 RETURN_VALUE # 返回值 None -------------------- 150 LOAD_GLOBAL0 (print) # 載入一個print函式 2 LOAD_FAST0 (admin) # 載入傳遞引數為admin 4 CALL_FUNCTION1 # 呼叫這個函式 6 POP_TOP # 從棧的頂端把元素移除出來 8 LOAD_CONST0 (None) # 因為該函式沒有返回任何值,所以載入的值是none 10 RETURN_VALUE # 最後把load_const的值返回(個人理解) None
程式碼函式執行的時候,Python 將程式碼編譯成位元組碼,當函式存在 yield 的時候,Python 會將這個函式標記成生成器,當呼叫這個函式的時候,會返回生成器物件,呼叫這個生成器物件後C語言中寫的函式會記錄上次程式碼執行到的位置和變數。
在 C 語言中的 PyGenObject 中有兩個值,gi_frame (儲存上次程式碼執行到的位置 f_lasti 的上次程式碼執行到的變數 f_locals),gi_code (儲存程式碼),使用 dis 也可以獲取到上次程式碼執行的位置和值。
舉個例子:
import dis def gen(): yield 1 yield 2 return 666 g = gen() # g是生成器物件 print(dis.dis(g)) print('*'*10) print(g.gi_frame.f_lasti) # 這裡還沒有執行,返回的位置是-1 print(g.gi_frame.f_locals) # 這裡還沒有執行,返回的物件是{} next(g) print('*'*10) print(g.gi_frame.f_lasti) print(g.gi_frame.f_locals)
返回結果:
110 LOAD_CONST1 (1) # 載入值為1 2 YIELD_VALUE 4 POP_TOP 126 LOAD_CONST2 (2) 8 YIELD_VALUE 10 POP_TOP 1312 LOAD_CONST3 (666) 14 RETURN_VALUE None ********** -1 # 因為還沒有執行,所以獲取的行數為 -1 {} ********** 2 # 這裡開始執行了第一次,獲取的行數是2,2對應2 YIELD_VALUE就是前面載入的數值1 {} # g.gi_frame.f_locals 是區域性變數,你都沒定義那麼獲取的結果自然是{},你只需在程式碼中加上user='admin',這裡的{}就會改變。
生成器可以在任何時候被任何函式恢復執行,因為它的棧幀實際上不在棧上而是在堆上。生成器在呼叫呼叫層次結構中的位置不是固定的,也不需要遵循常規函式執行時遵循的先進後出順序。因為這些特性,生成器不僅能用於生成可迭代物件,還可以用於實現多工協作。
就是說只要拿到了這個生成器物件,就能對這個生成器物件進行控制,比如繼續執行暫停等待,這個就是協程能夠執行的理論原理。
應用場景
讀取檔案,使用 open(‘xxx’).read(2019)// 開啟一個檔案,每次讀取 2019 個偏移量。檔案 a.txt 是一行文字,但是特別長,這一行文字根據|符號分開,如何讀取?
寫入檔案程式碼:
# -*- coding:utf-8 -*- import random import threading import string import time t1 = time.time() def write(x): with open('a.txt','a+')as a: a.write(x + '||') def run(): for x in range(10000000): strs = str(random.randint(1000,2000)) +random.choice(string.ascii_letters)*10 write(strs) for x in range(10): t = threading.Thread(target=run) t.start() t2 = time.time() print(t2 - t1)
讀取檔案程式碼:
# -*- coding:utf-8 -*- def readbooks(f, newline): # f為傳入的檔名,newline為分隔符 buf = "" # 快取,處理已經讀出來的資料量 while 1: while newline in buf: # 快取中的資料是否存在分隔符 pos = buf.index(newline) # 如果存在就找到字元的位置,比如0或者1或者2 yield buf[:pos] # 暫停函式,返回快取中的從頭到字元的位置 buf = buf[pos + len(newline):] # 快取變成了,字元的位置到末尾 chunk = f.read(2010 * 10) # 讀取2010*10的字元 if not chunk: # 已經讀取到了檔案結尾 yield buf break buf += chunk # 加到快取 with open('a.txt','r')as f: for line in readbooks(f,'||'): print(line)