for 迴圈跟 iterator (可走訪物件、迭代器)

很多人在初學 python 的時候,都會搞不懂 for 迴圈到底是在做些什麼
初學 python 應該會看到這樣的 for 迴圈

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

i 從 0 遞增到 9,然後每遞增一次,就執行 print(i) 。

那為什麼我們會搞不清楚 python for 呢?
因為以往的語言,裡面的 for 都指是 while 的精煉版,所以從 while 學到 for 並不會有不適應。
但是這不符合 python 中「所有的事情,都只用一個方法做到」。

而 python 更認為 for 的用途就是拿來把物件給走一遍
包括像是 [1, 2, 3] 的 list 或是 {1: 'a', 2: 'b'} 的 dict 還有剛剛談到的 0 ~ 9 的數字都適合拿來走訪

whlie 就專心在他的條件判斷,而 for 則是走一圈物件裡面所有的東西
那 for 到底是怎麼做到「走一圈」這件事呢?

簡單來講就是拿到 iterator (可走訪物件),然後走訪

for i in xxx: 來舉例
他做了兩個動作:

  1. 抓取 xxx 的可走訪物件,來判斷可否走訪:
    實際上是使用 iter(xxx) 去抓,其實動作也很簡單,就是從 xxx.__iter__() 取取看,如果有 __iter__() 這個方法,那就拿 __iter__() 回傳的可走訪物件來用,如果沒有 __iter__() 這個方法, iter(xxx) 就理所當然說 type error,這物件是無法被 for 迴圈走訪的。

  2. 開始走訪 iter() 回傳的物件,取得可走訪物件的下一個值:
    拿到回傳的可走訪物件之後,就會用 next(可走訪物件) ,去一次又一次的跑,然後把回傳的值丟給 i ,所以我們 for 迴圈裡面就能拿到 i 這個值,直到遇到 StopIteration 這個例外被丟出來為止,而 next() 做的事情不過就是抓取 可走訪物件.__next__() 回傳回來的東西而已。

所以我們的物件只要擁有__iter__()__next__() 這兩個方法,管他裡面有什麼黑箱子,都能夠被 for 拿來使用

以下使用 iter() 跟 next() 來做示範一個 python list 是怎麼讓 for 去走訪的

>>> it = iter([5, 6, 7, 8]) # 先抓他的可走訪物件
>>> next(it) # 取得可走訪物件的下一個值
5
>>> next(it)
6
>>> next(it)
7
>>> next(it)
8
>>> next(it) # 噴出 StopIteration 的例外
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

接著再往內透視一點點:

>>> it = [5, 6, 7, 8].__iter__()
>>> it.__next__()
5
>>> it.__next__()
6
>>> it.__next__()
7
>>> it.__next__()
8
>>> it.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

python 的作者 Guido 很喜歡這種黑箱子型別,又稱為 duck type,因為只要這東西會在水裡游、會呱呱叫、有翅膀,那我就把這東西當作鴨子。
因此我們接下來也做一隻可以被 for 迴圈拿來用的鴨子。

實作可走訪物件

我們來做一個類似 range 的事情好了, range(10) 的範圍是從 0 ~ 9 ,那我們現在設計的 myrange 做的事情跟 range 不同。
我希望 myrange 能給我起跳數字為 1 然後遞增到 10 為止 ( 1 ~ 10 )。

範例
class myrange:

    def __init__(self, start = 0, end = None, step = 1):
        # 判斷參數是否有提供 end,如果沒有提供 end 則 start 為結束值
        # 從 0 跑到 end
        if end == None:
            self.index = 0
            self.start = 0
            self.end = start

        # 三個參數都有提供的話,從 start 跑到 end 為止,每一次遞增 step
        else:
            self.index = start
            self.start = start
            self.end = end

        self.step = step

    def __iter__(self):
        # 當 iter() 呼叫時,會來這裡要一個可走訪的物件
        # 而我們也有 __next__() 方法可以讓人走訪,所以我們回傳 self 
        # 讓 next() 呼叫 self.__next__()
        # 同時代表著,我們也能不做出 __next__() ,而是回傳其他的可走訪物件
        # 像是回傳 [1, 2, 3].__iter__() 一樣也能被 for 迴圈走訪

        return self

    def __next__(self):
        self.index += self.step

        #記得要丟出 StopIteration 例外讓 for 迴圈停止,而這裡只是判斷超出範圍就丟例外
        if self.start < self.end and not self.start < self.index <= self.end:
            raise StopIteration

        elif self.start > self.end and not self.start > self.index >= self.end:
            raise StopIteration

        return self.index

    for i in myrange(10):
        print(i)

而我這邊不斷講到的可走訪物件,其實有個比較正式的翻譯叫做迭代器,就是每次都會把一個東西的回傳給你,不斷的代換。

而實作 __iter__() 出來的東西,就能玩完全全被 python 當成可迭代的物件,
而在範例的註解當中我也強調了一點, __next__() 可以不用被實作出來,可以替換成其他任何東西的迭代器
next() 去抓取其他物件的 __next__() 一樣能讓 for 迴圈跑起來,
這就是在 python 之中,duck type 被靈活運用的部份了