迭代器與可迭代對象
概念
迭代器:是訪問數據集合內元素的一種方式,一般用來遍歷數據,但是他不能像列表一樣使用下標來獲取數據,也就是說迭代器是不能返回的。
-
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() 即可。
在學習過程中有什么不懂得可以加我的
python學習交流扣扣qun,784758214
群里有不錯的學習視頻教程、開發工具與電子書籍。
與你分享python企業當下人才需求及怎么從零基礎學習好python,和學習什么內容
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 魔法函數
- 一般由可迭代對象變迭代器對象
可迭代對象
可迭代對象每次使用 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)
這里把注釋符去掉返回結果也是一樣的,返回結果:
1
2
3
迭代器對象
一開始提起,iter 搭配 Iterable 做可迭代對象,next 搭配 Iterator 做迭代器。next() 接受一個迭代器對象,作用是獲取迭代器對象的下一個值,迭代器是用來做迭代的,只會在需要的時候產生數據。
和可迭代對象不同,可迭代對象一開始是把所有的列表放在一個變量中,然后用 getitem 方法不斷的返回數值,getitem 中的 item 就是索引值。
但是 next 方法并沒有索引值,所以需要自己維護一個索引值,方便獲取下一個變量的位置。
在學習過程中有什么不懂得可以加我的
python學習交流扣扣qun,784758214
群里有不錯的學習視頻教程、開發工具與電子書籍。
與你分享python企業當下人才需求及怎么從零基礎學習好python,和學習什么內容
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
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)
返回結果:
123
這個時候是不能再用 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))
返回結果:
1
2
可以這么做,但是沒必要,因為這樣違反了設計原則。
迭代器的設計模式
迭代器模式:提供一種方法順序訪問一個聚合對象中的各種元素,而又不暴露該對象的內部
表示。
迭代器的設計模式是一種經典的設計模式,根據迭代器的特性(根據索引值讀取下一個內容,不一次性讀取大量數據到內存)不建議將 next 和 iter 都寫在一個類中去實現。
新建一個迭代器,用迭代器維護索引值,返回根據索引值獲取對象的數值,新建另一個可迭代對象,使用 iter 方法方便的循環迭代器的返回值。
生成器
生成器:函數中只要有 yield,這個函數就會變成生成器。每次運行到 yield 的時候,函數會暫停,并且保存當前的運行狀態,返回返回當前的數值,并在下一次執行 next 方法的時候,又從當前位置繼續往下走。
簡單用法
舉個例子:
在學習過程中有什么不懂得可以加我的
python學習交流扣扣qun,784758214
群里有不錯的學習視頻教程、開發工具與電子書籍。
與你分享python企業當下人才需求及怎么從零基礎學習好python,和學習什么內容
def gen():
yield 1
# 返回一個對象,這個對象的值是1
def ret():
return 1
# 返回一個數字1
g = gen()
r = ret()
print(g,r)
print(next(g))
返回結果:
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)
返回結果:
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))
返回結果:
1 0 LOAD_NAME 0 (langzi)
# 加載名字 為langzi
2 RETURN_VALUE
# 返回值
None
--------------------
15 0 LOAD_GLOBAL 0 (print)
# 加載一個print函數
2 LOAD_FAST 0 (admin)
# 加載傳遞參數為admin
4 CALL_FUNCTION 1
# 調用這個函數
6 POP_TOP
# 從棧的頂端把元素移除出來
8 LOAD_CONST 0 (None)
# 因為該函數沒有返回任何值,所以加載的值是none
10 RETURN_VALUE
# 最后把load_const的值返回(個人理解)
None
代碼函數運行的時候,Python 將代碼編譯成字節碼,當函數存在 yield 的時候,Python 會將這個函數標記成生成器,當調用這個函數的時候,會返回生成器對象,調用這個生成器對象后C語言中寫的函數會記錄上次代碼執行到的位置和變量。
在 C 語言中的 PyGenObject 中有兩個值,gi_frame (存儲上次代碼執行到的位置 f_lasti 的上次代碼執行到的變量 f_locals),gi_code (存儲代碼),使用 dis 也可以獲取到上次代碼執行的位置和值。
舉個例子:
在學習過程中有什么不懂得可以加我的
python學習交流扣扣qun,784758214
群里有不錯的學習視頻教程、開發工具與電子書籍。
與你分享python企業當下人才需求及怎么從零基礎學習好python,和學習什么內容
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)
返回結果:
11 0 LOAD_CONST 1 (1)
# 加載值為1
2 YIELD_VALUE
4 POP_TOP
12 6 LOAD_CONST 2 (2)
8 YIELD_VALUE
10 POP_TOP
13 12 LOAD_CONST 3 (666)
14 RETURN_VALUE
None
**********
-1
# 因為還沒有執行,所以獲取的行數為 -1
{}
**********
2
# 這里開始執行了第一次,獲取的行數是2,2對應2 YIELD_VALUE就是前面加載的數值1
{}
# g.gi_frame.f_locals 是局部變量,你都沒定義那么獲取的結果自然是{},你只需在代碼中加上user='admin',這里的{}就會改變。
生成器可以在任何時候被任何函數恢復執行,因為它的棧幀實際上不在棧上而是在堆上。生成器在調用調用層次結構中的位置不是固定的,也不需要遵循常規函數執行時遵循的先進后出順序。因為這些特性,生成器不僅能用于生成可迭代對象,還可以用于實現多任務協作。
就是說只要拿到了這個生成器對象,就能對這個生成器對象進行控制,比如繼續執行暫停等待,這個就是協程能夠執行的理論原理。
如果你依然在編程的世界里迷茫,可以加入我們的Python學習扣qun:784758214,看看前輩們是如何學習的。交流經驗。從基礎的python腳本到web開發、爬蟲、django、數據挖掘等,零基礎到項目實戰的資料都有整理。送給每一位python的小伙伴!分享一些學習的方法和需要注意的小細節,點擊加入我們的 python學習者聚集地
應用場景
讀取文件,使用 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)
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
