Python迭代器詳解

從for循環說起

我們都知道,在Python中,我們可以for循環去遍歷一個列表,元組或者range對象。

for i in [1,2,3]:
    print(i)
for i in range(0,10):
    print(i)

那底層的原理是什麼樣的呢?這其中涉及到了幾個概念,“可迭代”,“迭代器”,“生成器”等,大部分人可能聽過這些名詞,但是他們具體的含義以及之間的關係可能沒搞清楚,以下就是它們之間的關係圖,接下來我們就來分析這個關係圖。

Python迭代器詳解

可迭代對象與迭代器(先不關心左邊的生成器)

如果一個對象是可迭代對象,那麼我們就可以用for循環去遍歷它,比如列表、元組、字符串等都是可迭代對象。而我們用for循環去遍歷它的原理就是,先獲取了它的迭代器,然後使用迭代器的next方法去逐一遍歷。

a = [1,2,3]
# for相當於下面的代碼
for i in a:
    print(i)
# for循環分解(實際是通過Python底層C語言實現的,此處只是演示)

## 第一步: 獲取迭代器
iterator_a = iter(a)
## 第二步: 通過next逐個遍歷
while True:
    try:
        print(next(iterator_a))
    except StopIteration:  ## 第三步:遇到StopIteration異常,停止
        break

注意可迭代對象與迭代器的區別,如果一個對象是可迭代對象,那麼我們就肯定能調用iter()方法獲取它的迭代器,而如果一個對象是迭代器,我們就能用next()方法去拿下一個元素。 我們可以用isinstance判斷一個對象是不是可迭代對象,是不是迭代器。

>>> from collections.abc import Iterable
>>> from collections.abc import Iterator
>>> isinstance([1,2,3],Iterable)
True
>>> isinstance([1,2,3],Iterator)
False
>>> i = iter([1,2,3])
>>> isinstance(i, Iterable)
True
>>> isinstance(i, Iterator)
True
>>> type(i)
<class 'list_iterator'>

列表本身不是迭代器,它是可迭代對象,所以你不能用next()操作列表

>>> a=[1,2]
>>> next(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'list' object is not an iterator
>>> iter_a = iter(a)
>>> next(iter_a)
1
>>> next(iter_a)
2
>>> next(iter_a)   # 當沒有剩餘元素時,就拋出StopIteration異常
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

可迭代對象必須實現__iter__()函數,返回迭代器,調用對象自身的__iter__()函數與將iter()作用於對象效果是一樣的,同理對__next__()和next()也一樣。

>>> a=[1,2,3]
>>> iter_a = a.__iter__()
>>> next(iter_a)
1
>>> iter_a.__next__()
2

有趣的是,迭代器也是一個可迭代的對象,所以它本身也需要實現__iter__()函數,但是,一個迭代器的迭代器,是它本身,所以可能也有些多餘了。

>>> a=[1,2,3]
>>> iter_a = iter(a)
>>> iter_b = iter(iter_a)
>>> iter_c = iter(iter_b)
>>> iter_a is iter_b
True
>>> iter_a == iter_c
True
>>> for x in iter_a:
...    print(x)
...
1
2
3

一些的迭代器類型

我們經常會用到一些內置的迭代器,例如filter和map,注意range是可迭代對象,但不是迭代器。

>>> from collections.abc import Iterator
>>> from collections.abc import Iterable
>>> a=range(10)
>>> isinstance(b, Iterable)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'b' is not defined
>>> isinstance(a, Iterable)
True
>>> isinstance(a, Iterator)
False
>>> type(a)
<class 'range'>
>>> print(a)
range(0, 10)

filter

filter函數用於對一個列表進行過濾,傳入一個函數和列表,這個函數返回值是True或者False,將列表的元素逐個傳入這個函數,結果爲True的保留,可以使用lambda函數。

注:在Python2.x中返回值爲list,在Python3.x中返回迭代器

>>> from collections.abc import Iterator
>>> from collections.abc import Iterable
>>> a=filter(lambda x : x % 2 == 0, [1,2,3,4,5,6])
>>> isinstance(a, Iterable)
True
>>> isinstance(a, Iterator)
True
>>> type(a)
<class 'filter'>
>>> print(a)
<filter object at 0x000001A6100A2948>
>>> for i in a:
...  print(i)
...
2
4
6

如果我們要想通過下表訪問,可以把它轉換成list

>>> a=filter(lambda x : x % 2 == 0, [1,2,3,4,5,6])
>>> a = list(a)
>>> a[0]
2

map

map函數接收一個函數與一個列表,將這個函數作用域列表的每個元素,生成一個新的序列,返回迭代器。

>>> from collections.abc import Iterator
>>> from collections.abc import Iterable
>>> a=map(lambda x : x * x, [1,2,3])
>>> isinstance(a, Iterable)
True
>>> isinstance(a, Iterator)
True
>>> type(a)
<class 'map'>
>>>  print(i)
  File "<stdin>", line 1
    print(i)
    ^
IndentationError: unexpected indent
>>> print(a)
<map object at 0x000001A6100C5108>
>>> for i in a:
...   print(i)
...
1
4
9

實現一個可迭代對象與它的迭代器

我們將list做一個簡單的封裝,實現一個可迭代的mylist。

class mylist:
    def __init__(self, l):
        self.l = l

    def __iter__(self):
        return mylist_iterator(self.l)

class mylist_iterator:
    def __init__(self, l):
        self.l = l
        self.current = 0   # 記錄當前迭代到哪個元素了        
    def __iter__(self): # 迭代器的迭代器返回自己即可
        return self  
    def __next__(self):
        if self.current < len(self.l):
            self.current += 1
            return self.l[self.current-1]
        else:
            raise StopIteration

a = mylist([1,2])
for x in a:
    print(x)

i = iter(a)
print(next(i))
print(next(i))
print(next(i))

上述代碼並沒有實現迭代器帶來的好處,因爲我們事先傳入了一個列表進去,假如這個列表很大,會佔內存。假如我們要實現一個類似range()功能,我們可以使用更有效的方法。

class myrange:
    def __init__(self, max_num):
        self.max_num = max_num

    def __iter__(self):
        return myrange_iterator(self.max_num)

class myrange_iterator:
    def __init__(self, max_num):
        self.max_num = max_num
        self.current = 0   # 記錄當前迭代到哪個元素了        
    def __iter__(self): # 迭代器的迭代器返回自己即可
        return self  
    def __next__(self):
        if self.current < self.max_num:
            self.current += 1
            return self.current-1
        else:
            raise StopIteration

a = myrange(2)
for x in a:
    print(x)

i = iter(a)
print(next(i))
print(next(i))
print(next(i))

需要注意的是,我們的myrange不能隨機訪問,只能一次性順序遍歷,只能前進,不能後退,實際Python的range()可以隨機訪問。

for循環是否只能用於可迭代對象?

這個答案是No。for循環大部分情況都作用於可迭代對象,但是有一個例如,如果對象是可以通過下標訪問的,也能用於for循環。

一個對象如果不能用下標訪問,那麼就會報下面的錯誤,實際上它對應的是一個__getitem__()內置方法。

>>> a={1,2,3}
>>> a[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'set' object is not subscriptable
>>> a.__getitem__(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'set' object has no attribute '__getitem__'

如果我們實現了__getitem__(),也能通過for循環遍歷。

from collections.abc import Iterator
from collections.abc import Iterable
class mylist1:
    def __init__(self, l):
        self.l = l

    def __getitem__(self, i):
        return self.l[i]

print(isinstance(a, Iterable))
print(isinstance(a, Iterator))

a = mylist1([1,2,3])
for x in a:
    print(x)

結果如下,可以看到a既不是可迭代對象,也不是迭代器。

False
False
1
2
3

for循環會先看對象是不是實現了__iter__(),如果是,就用迭代器的方式,如果沒有的話,就看有沒有__getitem__(),都沒有就報錯,報的錯如下:

>>> for x in 5:
...   print(x)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable

那我們怎麼知道它先去找__iter__()呢?我們在前面的代碼里加上幾行,

from collections.abc import Iterator
from collections.abc import Iterable
class mylist1:
    def __init__(self, l):
        self.l = l

    def __getitem__(self, i):
        return self.l[i]
    def __iter__(self):
        return self
print(isinstance(a, Iterable))
print(isinstance(a, Iterator))
a = mylist1([1,2,3])
for x in a:
    print(x)

結果如下:

True
False
Traceback (most recent call last):

  File "<ipython-input-13-8e65b9d361a6>", line 1, in <module>
    runfile('C:/Users/xlniu/test123/test.py', wdir='C:/Users/xlniu/test123')

  File "C:\Users\xlniu\Anaconda3\lib\site-packages\spyder_kernels\customize\spydercustomize.py", line 827, in runfile
    execfile(filename, namespace)

  File "C:\Users\xlniu\Anaconda3\lib\site-packages\spyder_kernels\customize\spydercustomize.py", line 110, in execfile
    exec(compile(f.read(), filename, 'exec'), namespace)

  File "C:/Users/xlniu/test123/test.py", line 49, in <module>
    for x in a:

TypeError: iter() returned non-iterator of type 'mylist1'

特殊的迭代器-生成器(generator)

接下來我們看關係圖的左邊,生成器,生成器是迭代器,迭代器是可迭代對象,所以生成器肯定是可迭代對象了。哪些對象是生成器呢?

生成器的來源主要有兩個,一個是生成器表達式,例如(i for i in 'hello, world'), (i for i in range(0,10) if i % 2 == 0),另一個是生成器函數,生成器函數不使用return返回數據,而使用yield。

我們來看一下前面說的filter是不是生成器。

>>> from collections.abc import Iterator
>>> from collections.abc import Iterable
>>> from collections.abc import Generator
>>> a = [1,2,3,4,5,6]
>>> b = filter(lambda x : x % 2 == 0, a)
>>> print(isinstance(b, Generator))
False

它並不是一個生成器。

生成器表達式

生成器表達式與列表推斷是差不多的,但是它用"()"括起來,而列表推斷用的中括號,一般的語法就是:

expr(val) for val in xxx if yyy

例如

>>> from collections.abc import Generator
>>> a = (i for i in range(0, 10))
>>> next(a)
0
>>> next(a)
1
>>> print(isinstance(a, Generator))
True
>>> print(type(a))
<class 'generator'>
>>> print(a)
<generator object <genexpr> at 0x000001A61011B7C8>  #通過genexpr得到的生成器
>>> b = (i.upper() for i in 'hello, world')
>>> c = (x for x in range(0,10) if x % 5 == 0)
>>> for x in b:
...  print(x)
...
H
E
L
L
O
,

W
O
R
L
D
>>> d = [i for i in range(0,10)]
>>> print(type(d))
>>> type(d)
<class 'list'>
>>> print(d)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

如果我們把生成器表達式用在其他的對象上,例如set,list等,它們會自動轉換成相應類型。

>>> set(i for i in range(0,5))  # 相當於set( (i for i in range(0,5)) )
{0, 1, 2, 3, 4}
>>> set( (i for i in range(0,5)) )
{0, 1, 2, 3, 4}
>>> tuple(i for i in range(0,5))
(0, 1, 2, 3, 4)
>>> list(i for i in range(0,5))
[0, 1, 2, 3, 4]

生成器函數

另外一種生成器通過生成器函數得到。

from collections.abc import Iterator
from collections.abc import Iterable
from collections.abc import Generator

def myrange(stop):
    i = 0
    while i < stop:
        yield i     #返回i,下次調用時會從這個地方繼續向下執行
        i += 1

e = myrange(5)
print(isinstance(e, Iterable))
print(isinstance(e, Iterator))
print(isinstance(e, Generator))
print(type(e))
print(e)
print(next(e))
for x in e:
    print(x)

運行結果如下:

True
True
True
<class 'generator'>
<generator object myrange at 0x000001CEC7342C48>
0
1
2
3
4

在函數myrange中,有一個特殊的關鍵詞,yield。這個與return類似,但是return後,下次調用會從頭開始,但是使用了yield,我們的函數就會返回一個生成器,相當於每次執行,都記住了上次的位置,從上次的位置繼續執行。生成器表達式可以認爲是一種特殊的生成器函數,類似於lambda表達式和普通函數。但是和生成器一樣,生成器表達式也是返回生成器generator對象,一次只返回一個值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章