深度理解Python迭代器

雲崖先生發表於2021-05-20

迭代器

迭代是什麼

迭代指的是一個重複的過程,每次重複都必須基於上一次的結果而繼續,單純的重複並不是迭代,如Python中的for迴圈就是一個非常好的迭代例子。

for item in range(10):
    print(item)

迭代必須向前推進,不能後退,如下所示:

# [0 , 1, 2, 3, 4, 5, 6, 7, 8, 9]
# ------------------------------>

下面這種方式就不屬於迭代:

# [0 , 1, 2, 3, 4, 5, 6, 7, 8, 9]
# -------->
#     <----
#          --------------------->

迭代器協議

在學習迭代器的整個知識點中,迭代器協議佔據了非常重要的位置。

迭代器協議中包含了2個最基本的概念,分別是可迭代物件和迭代器物件。

  • 可迭代物件(Iterable):內部實現了__iter__()方法的物件則被稱為可迭代物件
  • 迭代器物件(Iterator):內部實現了__next__()方法的物件則被稱之為迭代器物件

兩者之間的關係:

  • 在Python中,迭代器物件一定屬於可迭代物件範疇,也就說迭代器物件必須具有__iter__()方法以及__next__()方法
  • 在Python中,可迭代物件不一定屬於迭代器物件範疇,也就是說可迭代物件只需要實現__iter__()方法即可

介紹2個函式:

  • iter(Object)函式,它底層會執行Object.__iter__()方法
  • next(Object)函式,它底層會執行Object.__next__()方法

內建型別

通過collections.abc下的Iterable類和Iterator類進行判定,可快速的判定出所有內建型別是否是一個可迭代物件或者迭代器物件:

>>> from collections.abc import Iterable
>>> from collections.abc import Iterator

>>> isinstance(list(), Iterable)
True
>>> isinstance(list(), Iterator)
False

經過測試,所有的容器型別(list、tuple、str、dict、set、frozenset)均屬於可迭代物件,但不屬於迭代器物件

原子型別(bool、int、float、None)等均不屬於可迭代物件,更不屬於迭代器物件。

也可以通過另一種方式進行驗證,通過hasattr()函式,檢查類中是否定義了某一個方法:

>>> hasattr(list,"__iter__")
True
>>> hasattr(list,"__next__")
False

for迴圈原理

當可迭代物件被for迴圈進行呼叫後,底層執行流程如下所示:

  1. 將自動的執行iter()方法,該方法內部會查詢可迭代物件的__iter__()方法,如果具有該方法,則返回一個該可迭代物件的專屬迭代器物件,如果沒有該方法,則丟擲TypeError object is not iterable的異常。

    Ps:每次的for迴圈都會返回一個全新的迭代器物件

  2. 不斷的呼叫迭代器物件的__next__()方法,並且返回迭代器物件中下一個資料項,當遍歷完成整個迭代器後,引發Stopiteration異常終止迭代

    Ps:迭代器本身並不儲存任何資料項,儲存的只是一個指標,該指標指向可迭代物件中真正儲存的資料項,它指向當前被遍歷到的資料項索引位置,下一次遍歷則向後推進這個位置

  3. for迴圈自動的捕捉Stopiteration異常,並且停止迭代

    Ps:for迴圈底層就是while迴圈實現的,只不過多加了3個步驟:

    第一步:執行可迭代物件的__iter()__方法並儲存返回的專屬迭代器

    第二步:不斷的執行迭代器的__next()__方法

    第三步:捕獲Stopiteration異常

我們手動的實現一個for迴圈:

li1 = list(range(10))

iteratorObject = iter(li1)  # ❶
while 1:
    try:
        print(next(iteratorObject))  # ❷
    except StopIteration as e:  # ❸
        break

❶:執行可迭代物件的__iter__()方法並儲存返回的專屬迭代器

❷:不斷的執行迭代器的__next__()方法

❸:捕獲Stopiteration異常

線性可迭代物件與迭代器的實現

如果是一個線性容器的可迭代物件,那麼它一定具有索引值,我們可以讓它的__iter__()方法返回一個專屬的迭代器物件。

然後專屬迭代器物件中記錄本次迭代遍歷的索引值,根據這個索引值返回可迭代物件中的資料項,當索引值達到可迭代物件中資料項總個數-1的時候,丟擲異常,本次迭代結束:

class linearTypeContainer:
    def __init__(self, array):
        if isinstance(array, list) or isinstance(array, tuple):
            self.array = array
        else:
            raise TypeError("argument array must is linear container")

    def __iter__(self):
        return linearContainer_iterator(self.array)  # ❶


class linearContainer_iterator:
    def __init__(self, array):
        self.index = 0
        self.array = array
        self.len = len(self.array)

    def __next__(self):
        if self.index < self.len:
            retDataItem = self.array[self.index]
            self.index += 1
            return retDataItem
        else:
            raise StopIteration

    def __iter__(self):  # ❷
        return self


container = linearTypeContainer([i for i in range(10)])
for i in container:
    print(i)
print(list(container))

❶:Python中的一切傳參均為引用傳遞

故linearTypeContainer中的self.array和linearContainer_iterator的self.array都是一個物件,並不會額外開闢記憶體空間

這也就是為什麼可迭代物件建立的專屬迭代器不會消耗太多的記憶體空間原因了。

❷:迭代器物件一定屬於可迭代物件範疇,所以在這裡我們為迭代器物件linearContainer_iterator類也新增了__iter__()方法

這樣做的好處在於如果單獨的拎出了這個迭代器物件,則它也會支援for迴圈的遍歷:

    def __iter__(self):  # ❷
        return self


containerIterator = linearTypeContainer([i for i in range(10)]).__iter__()


for item in containerIterator:
    print(item)
    
# 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9

如果取消了linearContainer_iterator類的這個__iter__()方法,則不支援for迴圈的遍歷:

...
    # def __iter__(self):  # ❷
    #     return self


containerIterator = linearTypeContainer([i for i in range(10)]).__iter__()


for item in containerIterator:
    print(item)

# TypeError: 'linearContainer_iterator' object is not iterable

非線性可迭代物件與迭代器實現

如果是一個非線性容器的可迭代物件,可以先判斷它的型別,如果傳入的容器是一個字典,則將迭代的資料項集合轉換為元組,裡面儲存的全部是字典的key即可。

如果傳入的容器是一個集合,則將迭代的資料項集合轉換為元組,再參照線性可迭代物件與迭代器的實現。

具體實現:

class mappingTypeContainer:
    def __init__(self, mapping):
        self.mapping = mapping
        self.mappingType = None

        if isinstance(mapping, dict):
            self.mappingType = "dict"

        elif isinstance(mapping, set) or isinstance(mapping, frozenset):
            self.mappingType = "set"

        else:
            raise TypeError("argument mapping must is mapping container")

    def keys(self):
        if self.mappingType == "set":
            raise TypeError("instance mapping type is set, no have method keys")
        else:
            return self.mapping

    def values(self):
        if self.mappingType == "set":
            raise TypeError("instance mapping type is set, no have method values")
        else:
            return self.mapping.values()

    def items(self):
        if self.mappingType == "set":
            raise TypeError("instance mapping type is set, no have method items")
        else:
            return self.mapping.items()

    def __str__(self):
        return str(self.mapping)

    def __iter__(self):
        return mappingContainer_iterator(tuple(self.mapping))


class mappingContainer_iterator:
    def __init__(self, array):
        self.index = 0
        self.array = array
        self.len = len(self.array)

    def __next__(self):
        if self.index < self.len:
            retDataItem = self.array[self.index]
            self.index += 1
            return retDataItem
        else:
            raise StopIteration

    def __iter__(self):
        return self


container = mappingTypeContainer({str("k") + str(i): str("v") + str(i) for i in range(3)})

for item in container.items():
    print(item)

print(container)

# ('k0', 'v0')
# ('k1', 'v1')
# ('k2', 'v2')
# {'k0': 'v0', 'k1': 'v1', 'k2': 'v2'}


container = mappingTypeContainer({i for i in range(3)})

for item in container:
    print(item)

print(container)

# 0
# 1
# 2
# {0, 1, 2}

迭代器物件的特性

每一次for迴圈建立出的可迭代物件的專屬迭代器都是一次性的,用完後就沒用了:

# ❶
containerIterator = linearTypeContainer([i for i in range(3)]).__iter__()


for item in containerIterator:
    print(item)

# 0
# 1
# 2

for item in containerIterator:
    print(item)  # ❷
    print("?")

❶:直接拿出一個迭代器物件

❷:在第2次迴圈中,迭代器物件中儲存的索引值已經最大了,每次呼叫iter()都會丟擲異常返回出來再被for處理,所以print()函式根本不會執行

迭代器物件並不儲存可迭代物件中的真正迭代資料,而是僅儲存長度和索引,所以記憶體的佔用並不多:

class linearContainer_iterator:
    def __init__(self, array):
        self.index = 0  # ❶
        self.array = array  # ❷
        self.len = len(self.array) # ❸
        
    ...

❶:佔用額外的記憶體空間

❷:引用物件,並不開闢記憶體

❸:佔用額外的記憶體空間

惰性求值與及早求值

迭代器物件中對於返回的資料項,是進行實時演算的,這種實時演算的特性求值方式被稱為惰性求值,即你需要的時候我算出來後再給你:

    def __next__(self):
        if self.index < self.len:
            retDataItem = self.array[self.index]
            self.index += 1
            return retDataItem
        else:
            raise StopIteration

除開惰性求值,還有一種及早求值的方案,即使你要1個,我也把所有的都給你。

如Python2中的range()、map()、filter()、dict.items()、dict.keys()、dict.values(),它們均返回的是一個純粹的列表,這樣的設計是不合理的。

因為返回的列表會佔用很大的記憶體空間,而Python3中則統一優化為惰性求值方案,即返回一個可迭代物件。

要命的問題

①:Python中的所有自帶容器型別為何不自己設定成迭代器?

而是在for迴圈時例項出一個專屬的迭代器?

直接在這些自帶型別的底層實現__next__()方法不好嗎?

這樣豈不是更加減少了記憶體的消耗,少定義了類和例項化了類嗎?

答:這真是一個要命的問題,這個問題我也想過很久,最後是在stackoverflow提問並且獲得了良好的解答才記錄下來的。

因為確實是可以實現的,如下所示,只需要在加上❶處程式碼即可:

class linearTypeContainer:
    def __init__(self, array):
        if isinstance(array, list) or isinstance(array, tuple):
            self.array = array
        else:
            raise TypeError("argument array must is linear container")

        self.index = 0
        self.len = len(self.array)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index < self.len:
            retDataItem = self.array[self.index]
            self.index += 1
            return retDataItem
        else:
            self.index = 0  # ❶
            raise StopIteration

container = linearTypeContainer(list(range(5)))

for item in container:
    print(item)

for item in container:
    print(item)

for item in container:
    print(item)

但是這樣做在某種特殊情況下會出現問題:

container = linearTypeContainer(list(range(5)))

for item in container:
    print(item)
    if container.index == 3:
        break

print("*"*20)

for item in container: 
    print(item)

# 0
# 1
# 2
# ********************
# 3
# 4

你會發現如果第一次for迴圈到了1半的時候退出,第二次for迴圈會接著根據第一次for迴圈進行繼續。

能夠解決一下嗎?只需要加上一個標誌位即可:

class linearTypeContainer:
    def __init__(self, array):
        if isinstance(array, list) or isinstance(array, tuple):
            self.array = array
        else:
            raise TypeError("argument array must is linear container")

        self.index = 0
        self.len = len(self.array)
        self.iter = False # ❶

    def __iter__(self):
        if self.iter: # ❷
            self.index = 0
        self.iter = True
        return self

    def __next__(self):
        if self.index < self.len:
            retDataItem = self.array[self.index]
            self.index += 1
            return retDataItem
        else:
            self.index = 0
            raise StopIteration


container = linearTypeContainer(list(range(5)))

for item in container:
    print(item)
    if container.index == 3:
        break

print("*" * 20)

for item in container:
    print(item)

# 0
# 1
# 2
# ********************
# 0
# 1
# 2
# 3
# 4

❶:判斷是不是一次新的呼叫

❷:如果是新的呼叫,則將index重新置為0即可

那麼為何Python不這樣設計呢?我們應該更多的考慮多執行緒的情況下,多個for迴圈使用同一個迭代器它是否是執行緒安全的,上面的示例中這個共享迭代器並不是執行緒安全的,此外它也不支援巢狀迴圈,如下所示,這樣會造成無限迴圈:

container = linearTypeContainer(list(range(5)))

for item in container:
    print(item)
    for j in container:
        print(j)

綜上各個方面的考慮,Python將內建的資料型別,都設定了在for迴圈時返回專屬迭代器的做法,這是非常好的設計,但是對於有些內建的物件,則是將它本身做成了迭代器,如檔案物件。

②:Python2中返回的及早求值物件,就沒有一點好處嗎?真的是浪費記憶體百無一用?

答:也不是,你可以發現Python3中所有返回的及早求值物件,都不支援索引操作,但是Python2中返回的由於是列表,它能夠支援索引操作,在某些極度極端的情況下這確實是個優勢,但是Python3的惰性求值物件需要這種優勢嗎?你手動將它轉換為list不香嗎?這樣提供給了你更多操作性的同時優化了記憶體佔用,何樂而不為呢?

③:你能實現一個返回惰性求值的物件嗎?

答:能啊!你看,我實現一個Range吧,其實就是傳參位置和自帶的不一樣,但是它是執行緒安全的且支援巢狀迴圈的:

class Range:
    def __init__(self, stop, start=0, step=1):
        self.start = start
        self.stop = stop
        self.step = step
        self.current = None

    def __iter__(self):
        return Range_iterator(self.stop, self.start, self.step)


class Range_iterator:
    def __init__(self, stop, start, step):
        self.start = start
        self.stop = stop
        self.step = step
        self.current = self.start

    def __next__(self):
        if self.current < self.stop:
            retDataItem = self.current
            self.current += self.step
            return retDataItem
        raise StopIteration


for i in Range(10):
    print(i)
    for j in Range(10):
        print(j)

相關文章