可迭代對象


先上一張圖,來描述一個大概的關系:

在Python世界里,一切皆對象。對象根據定義的維度,又可以分為各種不同的類型,比如:文件對象,字符串對象,列表對象。。。等等。

那什么對象才能叫做可迭代對象呢?實現了__iter__方法的對象就叫做可迭代對象,只有實現了__iter__方法的對象才能被for循環迭代。

 

1. 典型容器(container)

   容器就是一個用來存儲多個元素的數據結構,容器中的元素可通過迭代獲取,一次性加載所有元素到內存。

   for循環迭代的流程如下:

      1)調用可迭代對象的__iter__方法返回一個迭代器對象(iterator),這里便解釋了為什么只要實現了__iter__方法才能被for遍歷。

      2)不斷調用迭代器的__next__方法返回元素。

      3)直到迭代完成后,處理 StopIteration 異常。

     

      注:next()方法內部調用了對象的__next__()方法,iter()方法內部調用了對象的__iter__()方法。

from collections.abc import Iterable   # 可迭代對象
from collections.abc import Iterator   # 迭代器

x = [1, 2, 3]
print(isinstance(x, Iterable))   # True
print(isinstance(x, Iterator))   # False
for num in x:                    # 解釋器解釋完后其實變成這樣子:for num in iter(x),也等價於:for num in x.__iter__()
    print(num)

y = iter(x)       # 我們來直接操作返回的這個迭代器對象
print(next(y))    # 1
print(next(y))    # 2
print(next(y))    # 3

   Python內置的enumerate函數可以把一個list或者tuple變成可以返回索引-元素對的可迭代對象,這樣就可以在for循環中同時迭代索引和元素本身。

season = ['spring','summer','fall','winter']
print(enumerate(season))

"""
output:
<enumerate object at 0x00000217E72FDEC0>
"""

print(list(enumerate(season)))   # list函數可以將一個可迭代對象變成list

"""
output:
[(0, 'spring'), (1, 'summer'), (2, 'fall'), (3, 'winter')]
"""

for i, element in enumerate(season):
    print(i,element)
    
"""
output:
0 spring
1 summer
2 fall
3 winter
"""

 

2. 迭代器(Iterator)

   - 迭代器是一個帶狀態的對象。之所以說是帶狀態的對象是因為迭代器內部持有一個狀態,該狀態用於記錄當前迭代所在的位置,以方便下次迭代的時候獲取正確的元素。

    首先,迭代器必須能夠獲取下一個元素,故得有__next__方法,又迭代器本身也是可迭代對象,即也能被for循環遍歷,所以必須同時實現__iter__和__next__方法。

    容器內部只實現了__iter__方法,它的遍歷需要借助另外一個迭代器來完成。

       1)__iter__():該方法返回一個實例化的迭代器對象

       2)__next__():該方法返回下一個元素

       注:對容器而言:里面實現的 __iter__ 方法返回的迭代器不是自己,而是任意一個實現了 __next__ 方法的對象,兩個方法是分離的,換句話說:容器借助它人來

           幫自己完成遍歷。對迭代器而言:__iter__方法返回的迭代器實例就是自己,表示自身即是自己的迭代器,自己完成對自己的迭代

   - 迭代器不會一次性把所有元素加載到內存,而是需要的時候才生成返回結果(不同於容器)。

   - 迭代器實現了__next__方法,所以可以直接通過next()函數來返回下一個元素。

   下面我們來自定義一個迭代器類:

class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1  # 初始化兩個計數器 a,b

    def __iter__(self):
        return self            # 實例本身就是迭代對象,故返回自己。

    def __next__(self):
        # print("call __next__")
        self.a, self.b = self.b, self.a + self.b  # 計算下一個值
        if self.a > 10000:     # 退出循環的條件
            raise StopIteration()
        return self.a          # 返回下一個值

obj = Fib()
for n in obj:
    print(n)

# 第二次再迭代就沒有輸出了
for n in obj:
    print(n)

   a. 執行上面這個代碼的時候,為什么程序是正常結束的,而 StopIteration 異常沒有報出來?

      解釋:for會循環顯式地偵聽StopIteration。for語句的目的是循環由迭代器提供的序列,而StopIteration是正常的,預期的信號,告訴誰沒有什么更多的產物。

            for不捕獲被迭代的對象引發的其他異常,只捕獲 StopIteration。

   b. 為什么第二次迭代會沒有輸出呢?

      解釋:迭代器對象是一個帶狀態的不可逆的對象,由於第一次迭代就已經把所有元素都輸出了,狀態意味着結束了,第二次再迭代自然也就沒有元素輸出了。

            而容器是每次都會重新去獲取一個新的迭代器,它的迭代器與自身分離,故它們就可以多次遍歷。

            如果遇到第二次遍歷無輸出的一些問題,可以看看是迭代器對象還是容器對象。

alist = [1, 2, 3]
for x in alist:   # 等價於 for x in iter(alist),每次都返回一個新的迭代器
    print(x)

# 再遍歷,輸出依然正常
for x in alist:
    print(x)

 

3. 生成器(generator)

   為什么要有生成器?通過列表生成式,我們可以直接創建一個列表。但是,受到內存限制,列表容量肯定是有限的。而且,創建一個包含100萬個元素的列表,

   不僅占用很大的存儲空間,如果我們僅僅需要訪問前面幾個元素,那后面絕大多數元素占用的空間都白白浪費了。

   生成器其實是一種特殊的迭代器,不過這種迭代器更加優雅。它不需要再像上面的類一樣寫__iter__()__next__()方法了,只需要一個yiled關鍵字。

   生成器的特點和迭代器一樣:不會一次性把所有元素加載到內存,而是顯示或者隱式調用 next 的時候才執行代碼並生成返回結果(相同於迭代器,不同於容器)。

   要創建一個generator,有很多種方法。

   1)把一個列表生成式的[]改成(),就創建了一個generator

      generator保存的是算法,每次可以調用next(g),就計算出g的下一個元素的值,直到計算到最后一個元素,沒有更多的元素時,拋出StopIteration的錯誤。

      但是不斷調用next(g)實在是太麻煩,正確的方法是使用for循環,因為generator也是可迭代對象,即也實現了__iter__方法。

g = (x * x for x in range(10))
print(type(g))   # <class 'generator'>

for n in g:
    print(n)

   2)通過函數返回一個生成器對象

      理解生成器函數最重要的是理解它的執行流程:普通函數是順序執行,遇到 return 語句或者最后一行函數語句就返回。而生成器函數則類似於條件等待機制

      在函數執行過程中遇到 yield 時就返回並掛起函數,當再次調用 next 或者 for 循環取下一個元素(隱式調用next) 時,原本等待在 yield 處的函數

      就會繼續往下走,直到再遇到一個 yield。舉一個簡單的例子:

def fib(max_value):
    n, a, b = 0, 0, 1
    while n < max_value:
        yield b            # 此處會不停地掛起、執行、掛起、執行...直到拋出StopIteration
        a, b = b, a + b
        n = n + 1
    return 'done'

for x in fib(10):
    print(x)
  • 生成器也是可以嵌套的,就類似於遞歸,下面舉一個先序遍歷樹的例子,構造的樹形如下:

      ==> 先序遍歷輸出:0 1 3 4 2 5

class Node:
    def __init__(self, value):
        self._value = value
        self._children = []

    def __repr__(self):
        return 'Node({!r})'.format(self._value)

    def add_child(self, node):
        self._children.append(node)

    def __iter__(self):
        return iter(self._children)

    def depth_first(self):
        yield self
        for c in self:    # Node類實現了__iter__方法,故可以被for迭代,返回的是 self._children 的迭代器,即實際上迭代的是 []
            for e in c.depth_first():   # 由子迭代器返回元素
                yield e                 # 然后自身再返回子迭代器返回的元素,並掛起,子迭代器也遞歸掛起

root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
child1.add_child(Node(3))
child1.add_child(Node(4))
child2.add_child(Node(5))

for ch in root.depth_first():
    print(ch)         # 每次生成器返回一個 node 對象,並打印出該結點存儲的 value

"""
output:
Node(0)
Node(1)
Node(3)
Node(4)
Node(2)
Node(5)
"""

   生成器每次返回的是一個Node對象,那它是怎么嵌套的呢,下面給個示意圖幫助理解:

     

    由嵌套的生成器層層返回結果,並掛起自身,然后最上層的生成器再返回並掛起。

  • 迭代器內部的__iter__方法必須得返回迭代器實例,由於生成器是一個特殊的迭代器,所以可以將__iter__定義成生成器,for循環隱式調用next后得到一個生成器對象
class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):   # 定義成生成器,可以避免去實現__next__方法
        n = self.start
        while n > 0:
            yield n
            n -= 1

for rr in Countdown(30):
    print(rr)

     注:這種用類來封裝生成器函數的做法,也可以通過內部的屬性來記錄更多的信息或者狀態。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM