Python 中的可迭代對(duì)象、迭代器與生成器

前言

文章首發(fā)于個(gè)人公眾號(hào):可樂python說

Hi,大家好,我是可樂, 迭代器生成器 是 Python 學(xué)習(xí)中不可避免的兩個(gè)有趣的知識(shí)點(diǎn),實(shí)際開發(fā)中也比較常用。

我們?cè)谔幚泶罅繑?shù)據(jù)時(shí),有時(shí)會(huì)導(dǎo)致計(jì)算機(jī)內(nèi)存不足,我們需要將數(shù)據(jù)分塊處理,只處理所需的數(shù)據(jù),這將大大減少計(jì)算機(jī)內(nèi)存的消耗,這便是迭代器與生成器最直觀的作用。

今天,我們一起來探索 迭代器生成器 的相關(guān)知識(shí),并附上相關(guān)案例代碼,便于吸收、理解。

聊到這,我們不得不提起 可迭代對(duì)象 這個(gè)概念,首先我們用一張圖片來展示它們?nèi)咧g的關(guān)系。

可迭代對(duì)象

可迭代對(duì)象(Iteratable Object) 是能夠一次返回其中一個(gè)成員的對(duì)象,通常使用我們之前介紹過的 for 循環(huán) 來完成此操作,如字符串、列表、元組、集合、字典等等之類的對(duì)象都屬于可迭代對(duì)象。

簡(jiǎn)單來理解,任何你可以循環(huán)遍歷的對(duì)象都是可迭代對(duì)象。

1、使用 isinstance()函數(shù) 判斷對(duì)象是否是可迭代對(duì)象

# 導(dǎo)入 collections 模塊的 Iterable 對(duì)比對(duì)象
>>> from collections import Iterable
# 字符串是可迭代對(duì)象
>>> isinstance("kele", Iterable)
True
# 列表是可迭代對(duì)象
>>> isinstance(["kele"], Iterable)
True
# 字典是可迭代對(duì)象
>>> isinstance({"name":"kele"}, Iterable)
True
# 集合是可迭代對(duì)象
>>> isinstance({1,2}, Iterable)
True
# 數(shù)字不是可迭代對(duì)象
>>> isinstance(18, Iterable)
False

2、使用 dir()函數(shù) 查看對(duì)象內(nèi)所有的屬性與方法

# 字符串的所有屬性與方法
>>> dir("kele")
[..., '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', ...]

# 列表的所有屬性與方法
>>> dir(["kele"])
[..., '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__',...]

# 字典的所有屬性與方法
>>> dir({"name":"kele"})
[..., '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', ...]

# 數(shù)字的所有屬性與方法
# 并沒有找到 __iter__ 
>>> dir(18)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

3、對(duì)比可迭代對(duì)象與不可迭代對(duì)象的所有屬性與方法,我們發(fā)現(xiàn):可迭代對(duì)象都構(gòu)建了 __iter__ 方法,而不可迭代對(duì)象沒有構(gòu)建,因此我們也可通過此特點(diǎn)來判斷某一對(duì)象是不是可迭代對(duì)象。

4、我們來驗(yàn)證一下這個(gè)結(jié)論

# 沒有定義 __iter__ 方法則是不可迭代對(duì)象
>>> from collections import Iterable
>>> class IsIterable:
        pass
>>> isinstance(IsIterable(), Iterable)
False

# 定義 __iter__ 方法則是可迭代對(duì)象
>>> class IsIterable:
        def __iter__(self):
            pass
>>> isinstance(IsIterable(), Iterable)
True

5、看到這里,拋出一個(gè)思考, __iter__ 方法有什么作用,執(zhí)行它我們能得到什么?

# 調(diào)用后,得到了一個(gè)與調(diào)用對(duì)象對(duì)應(yīng)的對(duì)象 - iterator
>>> "kele".__iter__()
<str_iterator object at 0x0462CB30>
>>> ["kele"].__iter__()
<list_iterator object at 0x0462CA50>

這里得到的新對(duì)象,正是我們接下來要介紹的內(nèi)容 - 迭代器

迭代器

迭代器(Iterator) 是同時(shí)實(shí)現(xiàn)__iter__() 與 __next__() 方法的對(duì)象。

它可通過 __next__() 方法或者一般的 for 循環(huán)進(jìn)行遍歷,能夠記錄每次遍歷的位置,迭代器對(duì)象從集合的第一個(gè)元素開始訪問,直到所有的元素被訪問完結(jié)束,迭代器只能往前不能后退,終止迭代則會(huì)拋出 StopIteration 異常

1、迭代器是可迭代對(duì)象

>>> from collections import Iterable
# 以我們前面得到的迭代器為例
>>> isinstance("kele".__iter__(), Iterable)
True

2、使用 dir()函數(shù) 查看迭代器所有的屬性與方法

>>> dir("kele".__iter__(), Iterable)
# 我們可以看到迭代器同時(shí)實(shí)現(xiàn)
# __iter__ 與 __next__ 方法
[..., '__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__', ...]

3、使用 __next__() 方法獲取迭代器中的元素

>>> str_iterator = "kele".__iter__()
>>> str_iterator.__next__()
'k'
>>> str_iterator.__next__()
'e'
>>> str_iterator.__next__()
'l'
>>> str_iterator.__next__()
'e'
>>> str_iterator.__next__()
# 終止迭代則會(huì)拋出 StopIteration 異常
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration

4、使用 next()iter() 方法來實(shí)現(xiàn)相同的效果

# 使用 iter() 方法獲取一個(gè)迭代器
>>> str_iterator = iter("kele")
# 使用 next() 方法獲取迭代器中的元素
>>> next(str_iterator)
'k'
>>> next(str_iterator)
'e'
>>> next(str_iterator)
'l'
>>> next(str_iterator)
'e'
>>> next(str_iterator)
# 終止迭代則會(huì)拋出 StopIteration 異常
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration

5、自己動(dòng)手實(shí)現(xiàn)一個(gè)迭代器類,返回偶數(shù)

>>> class MyIterator:
        """
        迭代器類
        Author:可樂python說
        """
        # 類構(gòu)造函數(shù),調(diào)用時(shí)最先執(zhí)行
        # 用于分配執(zhí)行最初所需的任何值
        def __init__(self):
            self.num = 0
        # iter()和next()方法使這個(gè)類變成迭代器
        def __iter__(self):
            # 類本身就是迭代器,故直接返回本身
            return self
        def __next__(self):
            # 返回當(dāng)前值
            return_num = self.num
            # 并改變下一次調(diào)用的狀態(tài)
            self.num += 2
            return return_num
        
>>> my_iterator = MyIterator()
>>> next(my_iterator)
0
>>> next(my_iterator)
2
>>> next(my_iterator)
4
# 思考:for 循環(huán)為什么能夠自動(dòng)結(jié)束遍歷?

6、前文實(shí)現(xiàn)的迭代器類,并沒有寫結(jié)束的條件,這里優(yōu)化一下

>>> class MyIterator:
        """
        迭代器類
        Author:可樂python說
        """
        def __init__(self):
            self.num = 0
        def __iter__(self):
            return self
        def __next__(self):
            return_num = self.num
            # 只要值大于等于6,就停止迭代
            if return_num >= 6:
                raise StopIteration
            self.num += 2
            return return_num
        
>>> my_iterator = MyIterator()
>>> next(my_iterator)
0
>>> next(my_iterator)
2
>>> next(my_iterator)
4
>>> next(my_iterator)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration

7、我們還可對(duì)異常進(jìn)行處理,獲取到 StopIteration 異常便退出循環(huán)

>>> class MyIterator:
        # 以上略...
        def __next__(self):
            return_num = self.num
            # 只要值大于等于6,就停止迭代
            if return_num >= 6:
                raise StopIteration
            self.num += 2
            return return_num
        
>>> my_iterator = MyIterator()  
>>> while True:
        try:
            my_num = next(my_iterator)
        except StopIteration:
            break
        print(my_num)

0
2
4

我們對(duì)迭代器捕獲異常后,其實(shí)就是實(shí)現(xiàn)了與 for 循環(huán)類似的效果,這也正是 for 循環(huán)底層實(shí)現(xiàn)的方式,當(dāng)?shù)粋€(gè)可迭代對(duì)象時(shí),for 循環(huán)通過 iter() 方法獲取要迭代的項(xiàng),并使用 next() 方法返回后續(xù)的項(xiàng)。

迭代器可通過兩種方式獲?。阂环N是調(diào)用迭代器類中的方法直接返回迭代器,另一種是可迭代對(duì)象通過執(zhí)行 __ iter()__ 方法獲取,迭代器在一定程度上節(jié)省了內(nèi)存,需要時(shí)才去獲取對(duì)應(yīng)的數(shù)據(jù)。

在某些情況下,我們不想遵循迭代器協(xié)議,即不想實(shí)現(xiàn)__iter__() 與 __next__() 方法 ,但我們又想實(shí)現(xiàn)與迭代器相同的功能,這時(shí),就需要使用到一種特殊的迭代器,這正是我們接下來要介紹的內(nèi)容 - 生成器。

生成器

Python 中,提供了兩種 生成器(Generator) ,一種是生成器函數(shù),另一種是生成器表達(dá)式。

生成器函數(shù),定義與常規(guī)函數(shù)相同,區(qū)別在于,它使用 yield 語句 而不是 return 語句 返回結(jié)果, yield 語句一次返回一個(gè)結(jié)果,在每個(gè)結(jié)果中間,會(huì)暫停并保存當(dāng)前所有的運(yùn)行信息,以便下一次執(zhí)行 next() 方法時(shí)從當(dāng)前位置繼續(xù)運(yùn)行。

生成器表達(dá)式,與列表推導(dǎo)式類似,區(qū)別在于,它使用小括號(hào) - () 包裹,而不是中括號(hào),生成器返回按需產(chǎn)生結(jié)果的一個(gè)對(duì)象,而不是一次構(gòu)建完整的列表。

1、動(dòng)手實(shí)現(xiàn)一個(gè)生成器函數(shù)

>>> def my_generator():
        my_num = 0
        while my_num < 5:
            yield my_num
            my_num += 1
            
>>> generator_ = my_generator()
# 得到一個(gè)生成器對(duì)象
>>> type(generator_)
<class 'generator'>

2、生成器也是迭代器

# 以上略...
>>> generator_ = my_generator()
# 可發(fā)現(xiàn) __iter__ 與 __next__ 方法
>>> dir(generator)
[..., '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', ..., 'send', 'throw']

3、傳統(tǒng)方式獲取生成器的元素

# 以上略...
>>> generator_ = my_generator()
>>> next(generator_)
0
>>> next(generator_)
1
>>> next(generator_)
2
>>> next(generator_)
3
>>> next(generator_)
4
>>> next(generator_)
# 終止迭代則會(huì)拋出 StopIteration 異常
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration

4、使用 for 循環(huán)獲取生成器元素

# 以上略...
>>> generator_ = my_generator()
>>> for num_ in generator_:
        print(num_)

0
1
2
3
4

5、生成器表達(dá)式與列表生成式

聊到這,大家不妨思考一下,我們?yōu)槭裁匆褂蒙善鳎?/p>

我們以一個(gè)簡(jiǎn)單例子來對(duì)比一下,兩者實(shí)現(xiàn)相同功能的內(nèi)存消耗。

使用列表生成式獲取一個(gè)包括 100 萬個(gè)元素的列表,借用 sys 模塊計(jì)算內(nèi)存

>>> import sys
>>> my_list = [i for i in range(1000000)]
# 調(diào)用 sys.getsizeof() 獲取內(nèi)存消耗
>>> print("列表消耗的內(nèi)存:{}".format(sys.getsizeof(my_list)))
列表消耗的內(nèi)存:4348736

下面,我們看看生成器表達(dá)式

>>> import sys
>>> my_generator = [i for i in range(1000000)]
>>> print("生成器消耗的內(nèi)存:{}".format(sys.getsizeof(my_generator)))
列表消耗的內(nèi)存:56

很明顯,對(duì)于相同數(shù)量的項(xiàng),列表生成式和生成器在內(nèi)存消耗上存在巨大差異,這就是我們?yōu)槭裁匆褂蒙善鞯脑颉?/p>

應(yīng)用 - 使用 yield 實(shí)現(xiàn)斐波那契數(shù)列

斐波那契數(shù)列(Fibonacci sequence),又稱黃金分割數(shù)列、因數(shù)學(xué)家列昂納多·斐波那契(Leonardoda Fibonacci)以兔子繁殖為例子而引入,故又稱為“兔子數(shù)列”。

指的是這樣一個(gè)數(shù)列:1、1、2、3、5、8、13、21、34、……在數(shù)學(xué)上,斐波納契數(shù)列以如下被以遞推的方法定義:F(1)=1,F(xiàn)(2)=1, F(n)=F(n-1)+F(n-2)

今天,我們使用 Python 中的 yield 來實(shí)現(xiàn)

>>> def fibonacci(n):
        """斐波那契數(shù)列實(shí)現(xiàn)"""
        a, b = 0, 1
        while n > 0:
            a, b = b, a + b
            n -= 1
            yield a
# 獲取斐波那契數(shù)列前 10 個(gè)成員
>>> fibonacci_ = fibonacci(10)
    for i in fibonacci_:
        print(i)

1
1
2
3
5
8
13
21
34
55

擴(kuò)展 - itertools 庫(kù)簡(jiǎn)介

itertools 中的大多數(shù)函數(shù)是返回各種迭代器對(duì)象,如果自己去實(shí)現(xiàn)同樣的功能,代碼量會(huì)非常大,而在運(yùn)行效率上反而更低,因此,我們很有必要了解一下這個(gè)標(biāo)準(zhǔn)庫(kù)。

image

獲取指定數(shù)目?jī)?nèi)正整數(shù)的累加和

>>> import itertools
# 獲取 10 以內(nèi)的正整數(shù)累加和
>>> cumulative_sum = itertools.accumulate(range(10))
# 轉(zhuǎn)換為列表
>>> print(list(cumulative_sum))

[0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

獲取指定數(shù)目元素的所有排列(順序有關(guān))

>>> import itertools
# 獲取元素 1、2、3 的所有排列結(jié)果
>>> array_result = itertools.permutations((1, 2, 3))
# 轉(zhuǎn)換為列表
>>> print(list(array_result))

[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

總結(jié)

  1. 迭代器屬于可迭代對(duì)象,生成器是特殊的迭代器。
  2. 可迭代對(duì)象都構(gòu)建了 __iter__ 方法,迭代器還需構(gòu)建 __next__() 方法。
  3. 生成器是一種特殊的迭代器,內(nèi)部支持了生成器協(xié)議,不需要明確定義 __iter__ 方法和 __next__() 方法。
  4. 列表生成式的效率遠(yuǎn)高于 for 循環(huán)語句嵌套,生成器的效率遠(yuǎn)高于列表生成式。
  5. 獲取可迭代對(duì)象的元素時(shí),強(qiáng)烈推薦 for 循環(huán),因?yàn)樗邆渥詣?dòng)處理異常的能力。
  6. Python 中,包含 yield 關(guān)鍵詞的普通函數(shù)就是生成器。
  7. 文中難免會(huì)出現(xiàn)一些描述不當(dāng)之處(盡管我已反復(fù)檢查多次),歡迎在留言區(qū)指正,也可分享迭代器、與生成器相關(guān)的技巧、以及有趣的案例。
  8. 原創(chuàng)文章已全部更新至 Github :https://github.com/kelepython/kelepython
  9. 本文永久博客地址:https://kelepython.readthedocs.io/zh/latest/c01/c01_11.html
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容