Python系列(三):關於迭代器和生成器,你該瞭解這些

斯曦巍峨發表於2020-03-19

一.前言

說起迭代器和生成器,可以說是python語言的精髓之一,生成器可能有人沒用過,但是迭代器絕對是大家經常使用的(可能你並不瞭解自己正在使用迭代器),最常見的迭代器使用場景就是我們用for迴圈來遍歷各種列表,字串,元組等各種物件了。

mlist = [1,3,5,7,9,2,4,6,8,10]
for item in mlist:
    print(item)

string = 'hello world'
for s in string:
    print(s)

mtuple = ('c','c++','java','python')
for item in mtuple:
    print(item)

怎麼樣?是不是感覺很熟悉?而對於另一個生成器而言,我先不說太多,你第一印象只需記住它實質上也是一種迭代器,但它更簡潔。下面我將深入的為你剖析迭代器和生成器。

二.迭代器

2.1.迭代器物件(iterators)和可迭代物件(iterable)

2.1.1.辨析迭代器物件和可迭代物件:

  • 迭代器物件中定義了__next__()方法,每次呼叫該方法才會從容器中取出一個元素,當容器沒有元素可取時會丟擲StopIteration異常。
  • 可迭代物件中定義了__iter__()方法,該方法可以返回一個迭代器物件
  • 迭代器是一次性的,迭代器從第一個元素開始,直到所有的元素被訪問完,迭代器在訪問過程中只能往前不能退後。
  • 可迭代物件可以多次使用,每次使用iter()方法便可以從可迭代物件中返回一個迭代器。

頭兩點是區分迭代器物件和可迭代物件最好的方法,我們可以這樣理解:迭代器物件和可迭代物件都是用來迭代的,但迭代器物件可以直接通過__next__()方法進行物件中元素的迭代,而可迭代物件需要先使用__iter()__方法返回一個迭代器物件,然後就可以使用迭代器物件中的__next__()方法進行遍歷了。
程式碼示例

#在python中內建了iter()和next()函式,當使用這兩個函式時會分別呼叫__iter__(),__next__()方法。
mlist = [1,3,5,7]

print(next(mlist))
'''
直接使用該行程式碼會報錯:TypeError: 'list' object is not an iterator
因為mlist是一個列表,而列表不是一個迭代器並沒有實現__next__()方法
'''
#呼叫iter方法返回一個可迭代物件
my_itera = iter(mlist)
print(type(my_itera))
#<class 'list_iterator'>
print(next(my_itera))
#1
print(next(my_itera))
#3
print(next(my_itera))
#5
print(next(my_itera))
#7
print(next(my_itera))
#列表中已經沒有元素了,丟擲異常:StopIteration

2.1.2.常見的可迭代物件

在python中最常見的可迭代物件有:列表,元組,字串,集合,字典。

2.1.3.為什麼for迴圈能迭代可迭代物件

這裡直接給出官方的解釋:Behind the scenes, the for statement calls iter() on the container object. The function returns an iterator object that defines the method __next__() which accesses elements in the container one at a time. When there are no more elements, __next__() raises a StopIteration exception which tells the for loop to terminate.
這段話的大概意思是:for迴圈自動對可迭代物件呼叫了__iter__()方法返回一個迭代器物件,每次迴圈時又對該迭代器物件呼叫__next__()方法獲取容器中的一個元素,當容器中沒有元素時會丟擲StopIteration異常以終止迴圈。

2.2.如何自定義迭代器類

2.2.1.定義方法

前面我們已經介紹了:可迭代物件(Iterable)需要實現__iter__()方法,迭代器(iterator)需要實現__next__()方法。因此,通常是先實現迭代器類,然實現可迭代物件類,但這樣可能比較麻煩,因此如今基本上是同時在一個類中同時定義__iter__()和__next__()兩個方法,這樣__iter__()方法只需要返回本身就可以了。

2.2.2.程式碼示例

#定義一個輸出斐波那契數列的迭代器
class Fibonacci(object):
    """define a iterator class about fibnacci"""
    def __init__(self,num):
        self.a = 0
        self.b = 1
        self.num = num
	#定義__iter__方法
    def __iter__(self):
        return self
	#定義__next__方法
    def __next__(self):
        if self.a > self.num:
            raise StopIteration
        else:
            self.a,self.b = self.b,self.a + self.b
            return self.a

f = Fibonacci(10000)#指定數列的最大值不超過10000
for item in f:
    print(item,end=" ")
'''
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765  
'''

三.生成器

既然有了一般的迭代器那為什麼需要生成器呢?倘若你要自定義迭代器每次都要定義__iter__()方法和__next()__方法,這樣有點太麻煩了,但使用生成器卻不一樣,它的實現方式十分的簡潔,因為生成器會自動實現__iter__()和__next__()方法,並會在容器中沒有元素時自動丟擲StopIteration異常。這樣一來,就可以在減少程式碼量的情況下實現迭代器的功能。

3.1.什麼是生成器

這裡先給貼出一段程式碼,下面這段程式碼便實現了一個生成器(函式Fibonacci),同樣是實現一個可以用來迭代的物件,使用生成器來建立卻如此的簡潔。

def Fibonacci(num):
    a,b=1,1
    while b < num:
        a,b = b,a+b
        yield a

f = Fibonacci(10000)
for item in f:
    print(item,end=" ")
'''
1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 
'''

對於生成器的準確定義,這裡仍然引用官方文件的原話:Generators are a simple and powerful tool for creating iterators. They are written like regular functions but use the yield statement whenever they want to return data. 提煉出來的大致意思是:生成器是使用yield關鍵字來代替return的函式。

3.2.yield關鍵字

yield與return的第一個不同之處是yield返回的並不是值,而是一個生成器,這裡給出程式碼示例為:

def testYield(n):
    yield n

my_gen = testYield(10)
print(my_gen)
print(next(my_gen))
'''執行結果
<generator object testYield at 0x000001BB551C4BF8>
10
'''

第二個不同之處是:return關鍵字會立刻返回值然後結束函式,但在包含yield的函式(生成器)中,每次執行到yield函式都會暫停並儲存當前所有的執行資訊,並在下一次執行next()方法時從上一次暫停的位置繼續執行下去。程式碼示例為:

def testYield(n):
   for i in range(n):
    print('before yield')
    yield i
    print('after yield')

my_gen = testYield(10)
print(next(my_gen))
'''
before yield
0
'''
print(next(my_gen))
'''
after yield
before yield
1
'''
print(next(my_gen))
'''
after yield
before yield
2
'''

3.4.如何建立一個生成器

  • 從上面可以看出,在函式中使用yield就可以實現一個生成器,注意yield只能在函式中使用。
  • Python還提供了一種生成器的實現方式,這種方式類似於列表解析式,但使用圓括號"()“代替了方括號”[]",與列表解析式不同的是,生成器像列表解析式一樣一次性生成所有的元素的,而是一次生成一個,當你需要下一個時才會生成下一個,這種方式可以節約記憶體資源。

下面只演示第二種建立方式(第一種前面有栗子)

my_gen = (x**2 for x in range(1000000))
print(my_gen)
#<generator object <genexpr> at 0x000001BADC3F4BF8>
print(next(my_gen))
#0
print(next(my_gen))
#1
print(next(my_gen))
#4
print(next(my_gen))
#9

對於生成器,我在這裡推薦一個問題:列表展平問題,即將類似於[1,2,3,[4,5],[7,8]],[1,2,3,4,5,6,[[7,8,9,[10,11]]],[[[[12]]]]]這樣的列表展成維度為1的的列表,例如上述兩例分別展平為[1,2,3,4,5,6,7,8],[1,2,3,4,5,6,7,8,9,10,11,12]有興趣的可以看看自己能不能用生成器解決這個問題。

四.結語

善用迭代器和生成器可以幫助你寫出更簡潔,更漂亮的python程式碼。當然迭代器和生成器的用法遠遠不止如此,有要更深入瞭解的可以自行探索。

相關文章