Python實用教程系列——迭代器和生成器?

上篇文章我們介紹了Vscode搭建Python的開發(fā)環(huán)境,幫助了一些小伙伴解決了沒有Pycharm專業(yè)版的窘境,目前市面上出了一本《Visual Studio Code 權(quán)威指南》有興趣的小伙伴可以閱讀下。

大家還記得上篇文末留下的問題么?看到這里大家應(yīng)該就能想到那個問題是使用迭代器和生成器的相關(guān)知識點來解決問題啦。

一、迭代器

百度百科中對迭代器的一個解釋為:迭代器是一種對象,它能夠用來遍歷標(biāo)準(zhǔn)模板庫容器中的部分或全部元素,每一個迭代器對象代表容器中的確定的地址。

注意在Python中所有的集合都是可以迭代的,也就是說可以使用遍歷的方式去獲取其中的元素,那么在Python的中迭代器的支持哪些實現(xiàn)呢,比如:

(1)for循環(huán)

(2)按行遍歷文本文件

(3)列表、字典和集合推導(dǎo)式

(4)元組拆包

比如我們常用的for循環(huán)和文本讀取的操作:

for each in ['Python','學(xué)習(xí)']:
   print(each)
testfile = '1.txt'
with open(testfile) as f:
   for readline in f:
       print(readline)

可以看出Python的循環(huán)要比C語言的循環(huán)來的更加的抽象。

那么為什么迭代器支持這些實現(xiàn)呢?拿文本文件的遍歷來說,是因為文本對象支持(或者說實現(xiàn)了)迭代器的協(xié)議。

這里簡單的說明兩個協(xié)議:

  • 迭代器協(xié)議: 對象內(nèi)部實現(xiàn)了nextiter方法,其就是迭代器。

  • 可迭代協(xié)議: 對象內(nèi)部實現(xiàn)了iter方法,其就是可迭代對象。

1.1 可迭代對象

有些小伙伴在進行學(xué)習(xí)的時候,經(jīng)常會看到一個叫iterable的詞,看到這個的時候就說明我們遇到了“可迭代對象”,那么什么是可迭代對象,它有什么特點呢?

在有一些教材上將可直接使用for循環(huán)進行操作的對象為可迭代對象,其實這樣理解也沒有什么太大的問題。如果從可迭代對象的內(nèi)部的實現(xiàn)角度出發(fā),如果一個對象實現(xiàn)了iter方法,我們就可以認(rèn)為這個對象是可迭代的,這也正好與可迭代協(xié)議一致。

比如在list類中就實現(xiàn)了iter函數(shù):
?

補充一點:如果一個對象內(nèi)部實現(xiàn)了getitem方法,而且參數(shù)索引從0開始,那么這種對象也是可迭代的。

在實際的編碼開發(fā)過程中,有什么方法可以直接判斷一個對象是不是可迭代的對象呢?有的:就是使用isinstance()方法來進行判斷,比如:

from collections import Iterable
obj_1 = [1,2,3,4]
print(isinstance(obj_1,Iterable))

輸出True,這說明對象obj_1是可迭代的,實時上列表確實是可以迭代的。

在之前的推文中提到過,在Python中一切皆可為對象,接下來我們看一個示例:

from collections import Iterable
class Iterable_test:
   print('我愛python學(xué)堂')
   # 定義__iter__方法
   def __iter__(self):
       pass

f = Iterable_test()
print(isinstance(f,Iterable))

我們定義了一個類,并在類中實現(xiàn)了iter方法,接著我們實例化對象,最后我們判斷這個對象是否可迭代。結(jié)果為True,這正好驗證了上述的點:如果一個對象實現(xiàn)了iter方法,我們就可以認(rèn)為這個對象是可迭代的。

實際上我們還有一個另外一個方法可以進行判斷:使用iter()函數(shù):

num = 100
print(iter(num))
# TypeError: 'int' object is not iterable

輸出報錯這說明我們想判斷的對象不是可迭代對象,一般出現(xiàn)這樣的異常我們try/except處理一下就可以了。再例如:

obj_1 = [1,2,3,4]
print(iter(obj_1))
# <list_iterator object at 0x000001EC8EBC3C18>

這樣類似的輸出就說明我們想要判斷的對象是可迭代對象,使用iter()函數(shù)也是非常方便的。

1.2 迭代器

之前提到,迭代器是一種可以記住遍歷位置的對象,其內(nèi)部實現(xiàn)了iter方法和next方法。既然這樣,迭代器當(dāng)然也屬于可迭代對象了,只不過在Python中是通過可迭代對象來獲取迭代器的。

比如我們在使用for循環(huán)來遍歷string_1 = 'Python'字符串的時候,對象string_1的背后就是存在迭代器的,只是隱式的存在。

下面我們來介紹一下上面涉及到的兩個方法:next方法和iter方法,這也是標(biāo)準(zhǔn)的迭代器接口存在的兩個方法。

1.3 迭代器iter()方法

從迭代器和可迭代對象的角度來看,iter方法是迭代器與可迭代對象都要實現(xiàn)的方法,該方法返回的是當(dāng)前對象迭代器類的實例。直接返回self(即自己本身),以便在應(yīng)該使用可迭代對象的地方使用迭代器, 例如在 for 循環(huán)中。

1.4 迭代器next()方法

返回迭代過程中的每一步,如下一個可用的元素, 如果沒有元素了, 拋出 StopIteration異常。

這里拋出一個問題:python的迭代器為什么一定要實現(xiàn)iter方法?

簡單的解釋:如果迭代器不實現(xiàn)iter方法的話,那么其就不能迭代了,也就不是可迭代的對象。從另外一個角度上來看,在迭代器返回元素的過程中是先調(diào)用iter來獲取迭代對象,然后再調(diào)用可迭代對象的next來實現(xiàn)的。

最后我們來看看實際中我們怎么使用迭代器:

class Fib(object):
   def __init__(self):
       self.num_1, self.num_2 = 0, 1
   def __iter__(self):
       return self
   def __next__(self):
       self.num_1, self.num_2 = self.num_2, self.num_1 + self.num_2
       if self.num_1 > 100:
           raise StopIteration()
       return self.num_1

for each in Fib():
   print (each)

稍微解釋一下,每一次for循環(huán)的時候會調(diào)用Fib()中iter的方法,這個方法返回一個可迭代對象,然后調(diào)用該迭代對象的next()方法拿到返回值,直到遇到StopIteration錯誤時退出循環(huán),以上就是使用迭代器實現(xiàn)的Fib()函數(shù)。

實際上,我們可以使用迭代器來實現(xiàn)Fib()函數(shù)的,如下:

def Fib(n):
   num_1, num_2 = 0, 1
   while num_1 < 100:
       print(num_1)
       num_1, num_2 = num_2, num_1 + num_2
Fib(2)

當(dāng)然了,如果我們想將next()得到的值反向輸出,我們就可以使用reversed()創(chuàng)建

反序訪問的迭代器。

我們分步驟看看next()會的實際效果:

S = 'python'
S_1 = iter(target)
print(next(S_1)) #p
print(next(S_1)) #y
print(next(S_1)) #t
print(next(S_1)) #h
print(next(S_1)) #o
print(next(S_1)) #h
print(next(S_1)) # Traceback (most recent call last)...StopIteration

二、生成器

百度百科中對生成器(generator)的一個解釋為:生成器是一次生成一個值的特殊類型的函數(shù),可以將其視為可恢復(fù)函數(shù),調(diào)用該函數(shù)將返回一個可用于生成連續(xù)目標(biāo)值的生成器,在函數(shù)的執(zhí)行過程中,yield語句會把你需要的值返回給調(diào)用生成器的地方,然后退出函數(shù),下次調(diào)用生成器函數(shù)的時候又從上次中斷的地方開始執(zhí)行,而生成器內(nèi)的所有變量參數(shù)都會被保存下來供下次調(diào)用的時候使用。

2.1 關(guān)鍵詞yield

作用: yield 的作用就是把一個函數(shù)變成一個生成器,實現(xiàn)了yield 的函數(shù)不再是一個普通函數(shù),Python的解釋器會將其視為一個生成器?;蛘哒fyield是一個具有return功能的關(guān)鍵字,只是含有yield的函數(shù)返回的是個生成器。

一般情況下,包含yield 的函數(shù)我們成為生成器函數(shù),一般包含return 的函數(shù)我們就認(rèn)為是一般函數(shù)。

我們來看一個例子:

def myGenerator() :
   mylist = range(1,3)
   for i in mylist :
       yield i*i

test_generator = myGenerator()
print('test_generator:',test_generator)
for i in test_generator:
    print(i)

上述輸出:

test_generator: <generator object myGenerator at 0x000001FACC866DB0>

1

4

可以看出,對象test_generator是一個generator。唯一要注意的是:當(dāng)我們的代碼運行到test_generator = myGenerator()的時候,其實函數(shù)內(nèi)部由于存在yield代碼并不會馬上就執(zhí)行 ,這個函數(shù)只是返回一個生成器對象。然后我們使用for循環(huán)來讀取生成器對象中的值。

那么使用生成器有什么好處呢?我覺得有兩個好處:

  1. 精簡代碼量

  2. 提高代碼性能

2.2 精簡代碼量

比如我們之前在上述使用迭代器實現(xiàn)的Fib的前N數(shù),我們使用的代碼量還是很多的,如果我們使用yield的話就非常舒服了:

def Fib(n):
   num_1, num_2 = 0, 1
   while num_1 < n:
       yield num_1
       num_1, num_2 = num_2, num_1 + num_2

for n in Fib(4):
   print(n)

注意的是我們?nèi)匀皇褂脃ield構(gòu)造出了一個生成器函數(shù),通過for循環(huán)來進行數(shù)據(jù)的提取。

2.3 提高代碼性能

第二個優(yōu)點就是提高性能,這也是解決我們上次推文中提及到的問題的關(guān)鍵。我們試想一下在N(假設(shè)N>50)個數(shù)據(jù)中,這樣取數(shù):任意選2個、任意選3個

…任意選N個,把所有這樣選擇的數(shù)都加起來,我們知道這個數(shù)量是非常大的,所有把所有的組合存入列表丟到內(nèi)存中,這很有可能將內(nèi)存撐爆的,因此我們有必要來優(yōu)化性能。

那么使用yield的生成器實際上是怎么優(yōu)化性能的呢?我們來看看怎么實現(xiàn)的:

import itertools
def onegenerate(string_index):
   #for number_1 in range(2, len(string_index) + 1):
   for number_1 in range(len(string_index), 1, -1):
       iter = itertools.combinations(string_index, number_1)
       iter_list = list(iter)
       for number_2 in range(len(iter_list)):
           each = iter_list[number_2]
           each_list = list(each)
           yield each_list

list_test = [i for i in range(1,21)]

mygene = onegenerate(list_test)
print(mygene)
total = 0
for n in mygene:   #迭代器里面的內(nèi)容
   # print(n)
   total = total + 1
print(total)

大家可以試著運行一下輸出是什么,這里就不告訴大家輸出是什么了。

輸出的結(jié)果是反向的,我們可以使用reversed()進行反轉(zhuǎn)的。

寫到這里,還有一個實際項目中用到的技巧,假設(shè)我們電腦內(nèi)存是8G,我們遇到了16G大小的數(shù)據(jù),這怎么辦呢?沒錯,我們可以使用分塊讀取的技巧。也就是使用答案是使用 yield 構(gòu)造 generator,可以這樣:

def chunk_readtest(file, CHUNK_SIZE):
   while True:
       chunk_data = file.read(CHUNK_SIZE)
       if chunk:
           yield chunk_data
       else:
           return

f = open('yourdata')
# chunk_size為每次讀入數(shù)據(jù)的塊大小
CHUNK_SIZE = 2048
for chunk in chunk_readtest(f,CHUNK_SIZE):
   # 使用數(shù)據(jù)的函數(shù)
   use_chunk(chunk)

這個技術(shù)在我們處理大數(shù)據(jù)的時候非常有用的哦。

三、總結(jié)

以上就是迭代器和生成器的全部內(nèi)容,其實它們也不是很難,個人認(rèn)為在實際的項目中生成器用的還是多一點,大家可以將重點放在生成器的使用上。對于迭代器,大家也要掌握好,因為它能讓我們明白為什么可以使用for循環(huán)來進行迭代取值。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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