python 中的迭代器和生成器簡單介紹

海天之涯發表於2022-03-06
可迭代物件和迭代器

迭代(iterate)意味著重複,就像 for 迴圈迭代序列和字典那樣,但實際上也可使用 for 迴圈迭代其他物件:實現了方法 __iter__ 的物件(迭代器協議的基礎)。
__iter__方法返回一個迭代器,它是包含方法 __next__ 的物件,呼叫時可不提供任何引數;
當你呼叫 __next__ 時,迭代器應返回其下一個值;如果沒有可供返回的值,應引發 StopIteration 異常;
也可使用內建函式 next(),此種情況下,next(it) 與 it.__next()__ 等效。

至於為什麼不用列表?因為在很多情況下,使用列表都有點太浪費了。例如,如果你有一個可逐個計算值的函式,你可能只想逐個地獲取值,而不是使用列表一次性獲取。這是因為如果有很多值,列表可能佔用太多的記憶體。
下面來看一個不能使用列表的示例,因為如果使用,這個列表的長度將是無窮大的!

# 這個“列表”為斐波那契數列,表示該數列的迭代器如下:
class Fibs:
    def __init__(self):
        self.a = 0
        self.b = 1

    def __next__(self):
        self.a, self.b = self.b, self.a + self.b
        return self.a  # 前面邏輯自定義,最後返回下一個值即可

    def __iter__(self):
        return self  # 返回迭代器本身(一個包含 __next__ 方法的物件)
    
fibs = Fibs()

for f in fibs:
	if f > 1000:
		print(f)  # 1597
		break  # 若不中斷迴圈,將一直迴圈下去
        
next(fibs)  # 2584
next(fibs)  # 4181

更正規的定義是,實現了方法 __iter__ 的物件是 可迭代的,再實現了方法 __next__ 的物件是 迭代器。


內建函式 iter()

通過對可迭代物件呼叫內建函式 iter(),可以獲得一個迭代器。還可使用它從函式或其他可呼叫物件建立可迭代物件。
不過,可迭代物件在轉化為迭代器後,會丟失⼀些屬性(如 __getitem__() ),但同時也會增加⼀些屬性(如 __next__() )。
另外,迭代器一般都是⼀次性的,當迭代過⼀輪後,再次迭代將獲取不到元素;而可迭代物件可以重複使用。

it = iter([1, 2, 3])  # list 是可迭代物件哦
next(it)  # 1
next(it)  # 2
next(it)  # 3
next(it)  # StopIretation; 普通的可迭代物件是可複用的,而迭代器是一次性的,回不了頭的

it = iter("ABCD")  # string 也是可迭代物件
for i in it:
    print(i, end=" ")  # A B C D
for i in it:
    print(i, end=" ")  # ⽆輸出

檢視物件是否實現了魔法方法 _iter_ 的四種方法:

# ⽅法1:dir()檢視__iter__,詳情請自己嘗試
dir(2) # 沒有
dir("abc") # 有 __iter__()

# ⽅法2:isinstance()判斷
import collections
isinstance(2, collections.Iterable) # False
isinstance("abc", collections.Iterable) # True

# ⽅法3:hasattr()判斷
hasattr(2,"__iter__") # False
hasattr("abc","__iter__") # True

# ⽅法4:⽤iter()檢視是否報錯
iter(2) # 報錯:'int' object is not iterable
iter("abc") # <str_iterator at 0x1e2396d8f28>

從迭代器建立序列

在可以使用序列的情況下,大多也可以使用迭代器或可迭代物件(諸如索引和切片等操作除外)。迭代器因為缺少 __getitem__ ,因此不能使⽤普通的切⽚語法,暫未深究。

# 使用建構函式 list() 顯示的將迭代器轉換為列表
class TestIterator:
    value = 0
    
    def __next__(self):
        self.value += 1
        if self.value > 10: raise StopIteration
        return self.value
    
    def __iter__(self):
        return self
    
ti = TestIterator()
ti2 = list(ti)  # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

for i in ti2:
    print(i, end=" ")  # 1 2 3 4 5 6 7 8 9 10
        
print('the second:')

for i in ti2:
    print(i, end=" ")  # 1 2 3 4 5 6 7 8 9 10
生成器

生成器,也被稱為簡單生成器(simple generator),生成器自動建立了 iter() 和 next() 方法,是一種使用普通函式語法定義的迭代器。與函式的主要的形式差別就在於,它的函式體中有一句 yield 語句。
每次執行到 yield 處時,生成並返回一個值後,函式都將暫時停止執行,等待下一輪迭代呼叫,如此往復,直到迭代完。資料量大時,生成器能夠極大地節省記憶體空間。下面還是通過斐波納契數列來看看:

# 斐波納契數列的生成器實現: 返回數列的前 n 項
def fibs(n):
    a, b = 0, 1
    for _ in range(n):
        yield a  # 返回的是一個生成器
        a, b = b, b+a

f = fibs(5)
print(f)  # <generator object fibs at 0x05BB20B0>
print(list(f))  # [0, 1, 1, 2, 3]; 此處生成器 f 已經被迭代過一次了

for i in f:
    print(i, end=" ")  # ⽆輸出; for迴圈會⾃動捕捉到 StopIteration 異常並停⽌調⽤ next()
    
print(next(f))  # StopIteration

與 return 的區別:生成器不是像 return 一樣返回一個值,而是可以生成多個值,每次返回一個;return 返回的話,這個函式就結束了。

生成器推導(生成式表示式)

將列表生成式的 [] 改成 () 之後,資料結構將從列表變為生成器,而不是元組。如果要包裝可迭代物件(可能生成大量的值)時,若使用列表推導將立即例項化一個列表,從而喪失迭代的優勢;但如果使用生成器推導的話,每迭代一次就生成一個值,沒必要一次性生成全部值,這樣就好的多了。而且,可以直接在既有的圓括號內(如在函式呼叫中)使用生成器推導時,無需再新增一對圓括號。

L = [x*x for x in range(10)]  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

g = (x*x for x in range(10))  # <generator object <genexpr> at 0x052AF8F0>
print(next(g))  # 0

sum(i ** 2 for i in range(10))  # 285
遞迴式生成器

建立一個將兩層巢狀列表展開的函式:

nested = [[1, 2], [3, 4], [5], 6]

def flatten(nested):
    try:
        for sub in nested:
            for ele in sub:
                yield ele
    except TypeError:
        yield sub
            
f = flatten(nested)
next(f)  # 1
# print(list(f))  # [2, 3, 4, 5, 6]

for i in f3:
    print(i)  # 2 3 4 5 6

建立一個將任意層巢狀列表展開的函式:
對一層列表進行遍歷,遍歷下層列表的時候,先對一層遍歷出來的元素再呼叫一次 flatten 函式,這時,如果是不可再迭代的物件的話,就會報 TypeError 錯誤,捕捉到之後,yeild 返回,繼續下一個;如果是可迭代的話,就遞迴下去;

def flatten(nested):
    try:
        for sub in nested:
            for ele in flatten(sub):
                yield ele
    except TypeError:
        yield nested
        
nested = [[[1], 2], 3, 4, [5, [6, 7]], 8]
print(list(flatten(nested)))

不過,上面要注意的是:前面也提到了,字串物件也是可迭代的,而且一般我們也不會將它拆開。更重要的是,字串的第一個元素是一個長度為 1 的字串,而長度為 1 的字串的第一個元素是字串本身。

s = 'ABCD'
s2 = s[0]  # 'A'
s2[0]  # 'A'

這樣子會造成無窮遞迴的。所以還需要檢查一下物件是否類似於字串:

def flatten(nested):    
    try:
        if isinstance(nested, str): raise TypeError
            
        for sub in nested:
            for ele in flatten(sub):
                yield ele
    except TypeError:
        yield nested
        
nested = [[[1], '23'], 3, '43', [5, [6, '73']], 8]
print(list(flatten(nested)))  # [1, '23', 3, '43', 5, 6, '73', 8]

不過,它有兩個 yield 唉,這認哪個來著???pass

def flatten(nested):
	try:
		for sublist in nested:
			for element in flatten(sublist):
				print("element:", element)
				yield element
	except TypeError:
		print("nested :", nested)
		yield nested
		
print(list(flatten([[1, 2], [3, 4], [5], 6])))

輸出:

nested : 1
element: 1
element: 1
    
nested : 2
element: 2
element: 2
    
nested : 3
element: 3
element: 3
    
nested : 4
element: 4
element: 4
    
nested : 5
element: 5
element: 5
    
nested : 6
element: 6
[1, 2, 3, 4, 5, 6]

相關文章