獨家 | 什麼是Python的迭代器和生成器?(附代碼)


作者:Aniruddha Bhandari

翻譯:王琦

校對:和中華

本文約3700字,建議閱讀10分鐘。

本文介紹了Python中的生成器和迭代器。在處理大量數據時,計算機內存可能不足,我們可以通過生成器和迭代器來解決該問題。

迭代器:一次一個!

Python 是一種美麗的編程語言。我喜歡它提供的靈活性和難以置信的功能。我喜歡深入研究Python的各種細微差別,並瞭解它如何應對不同的情況。

 

在使用Python的過程中,我瞭解到了一些功能,這些功能的使用與其簡化的複雜度不相稱。我喜歡稱它們爲Python中“隱藏的寶石”。很多人對此並不瞭解,但對於分析和數據科學專家來說,它們非常有用。

 

Python迭代器和生成器正好屬於這一類。它們的潛力是巨大的!

 

如果你曾經在處理大量數據時遇到麻煩(誰沒有呢?!),並且計算機內存不足,那麼你會喜歡Python中的迭代器和生成器的概念。

 

與其將所有數據一次性都放入內存中,不如將它按塊處理,只處理當時所需的數據,對嗎?這將大大減少我們計算機內存的負載。這就是迭代器和生成器的作用!

 

因此,讓我們仔細讀讀本文,探索Python迭代器和生成器的世界吧。

 

我假設你熟悉Python的基礎知識。如果沒有,我建議你先從下面的熱門課程學起:

Python數據科學:

https://courses.analyticsvidhya.com/courses/introduction-to-data-science?utm_source=blog&utm_medium=python-iterators-and-generators

這是我們要介紹的內容:

  • 什麼是可迭代對象?

  • 什麼是Python迭代器?

  • 在Python中創建一個迭代器

  • 熟悉Python中的生成器

  • 實現Python中的生成器表達式

  • 爲什麼你應該使用迭代器?

什麼是可迭代對象?

可迭代對象是能夠一次返回其一個成員的對象”。

 

通常使用for循環完成此操作。像列表、元組、集合、字典、字符串等等之類的對象被稱爲可迭代對象。簡而言之,任何你可以循環的對象都是可迭代對象。

 

我們可以使用for循環逐個地返回可迭代的元素。在這裏,我們使用for循環遍歷列表的元素:

# iterables  
sample = ['data science', 'business analytics', 'machine learning']  
for i in sample:  
    print(i)

既然我們知道了什麼是可迭代對象,那麼實際上我們是如何遍歷這些值的?以及我們的循環如何知道何時停止?進入到迭代器部分!

 

什麼是Python迭代器?

迭代器是代表數據流的對象,即可迭代。它們在Python中實現了迭代器協議。這是什麼?

 

好吧,迭代器協議允許我們在一個可迭代對象中使用兩種方法來循環遍歷項:__iter __()和__next __()。所有的可迭代對象和迭代器都有__iter __()方法,該方法返回一個迭代器。

 

迭代器跟蹤可迭代對象的當前狀態。

 

但可迭代對象和迭代器不同之處在於__next __()方法只能由迭代器訪問。這使得無論何時只要我們要求迭代器返回下一個值,迭代器就會返回下一個值。

 

讓我們創建一個簡單的可迭代對象、本例中爲一個列表以及使用__iter __()方法來構造一個迭代器來了解其工作原理:

sample = ['data science', 'business analytics', 'machine learning']  
# generating an iterator  
it = sample.__iter__()  
print(it)  
# iterables do not have __next__() method  
sample.__next__()  

 

是的,正如我所說,可迭代對象有用於創建迭代器的__iter __()方法,但它們沒有僅迭代器纔有的__next __()方法。因此,讓我們再試一次,然後嘗試從列表中檢索值:

sample = ['data science', 'business analytics', 'machine learning']  
# generating an iterator  
it = sample.__iter__()  
print(it.__next__())  
print(it.__next__())  
print(it.__next__()) 

 

完美!但等一下,我不是說迭代器也具有__iter __()方法嗎?那是因爲迭代器也是可迭代的,但反過來不成立。它們是自己的迭代器。讓我通過遍歷迭代器向你展示這個概念:

sample = ['data science', 'business analytics', 'machine learning']  
it = sample.__iter__()  
itit = it.__iter__()  
print(type(itit))  
print(itit.__next__())  
print(itit.__next__())  
print(itit.__next__())  

酷!但我們可以使用iter()和next()來代替__iter__()和__next__()方法,它們提供了一種更簡潔的方法:

sample = ['statistics', 'linear algebra', 'probability']  
  
# iterator  
it = iter(sample)  
  
# next values  
print(next(it))  
print(next(it))  
print(next(it))

 

但如果我們超過了調用next()方法的限制次數,該怎麼辦?這會發生什麼呢?

 

print(next(it))  

是的,我們得到了一個錯誤!如果我們在到達迭代器的末尾之後嘗試訪問下一個值,則會引起StopIteration異常,該異常的意思是“你不能更進一步了!”。

 

我們可以使用異常處理來處理此錯誤。實際上,我們可以自己構建一個循環來遍歷可迭代的項:

sample = ['statistics', 'linear algebra', 'probability']  
it = iter(sample)  
while True:  
    # this will execute till an error is raised  
    try:  
        val = next(it)  
    # when we reach end of the list, error is raised and we break out of the loop  
    except StopIteration:  
        break  
    print(val) 

如果你退後一步,你會意識到,這正是for循環在底層運行的方式。我們在此處手動循環中所做的操作,for循環會自動執行相同的操作。這就是爲什麼for循環比遍歷可迭代對象更可取,因爲它們會自動處理異常。

 

每當我們迭代一個可迭代對象時,for循環通過iter()知道要迭代的項,並使用next()方法返回後續的項。

在Python中創建一個迭代器

既然我們知道了Python迭代器是如何工作的,我們可以更深入地研究並從頭開始創建一個迭代器,以更好地瞭解其是如何湊效的。

 

我將創建一個用於打印所有偶數的簡單迭代器:

class Sequence():  
    def __init__(self):  
        self.num = 2  
    def __iter__(self):  
        return self  
    def __next__(self):  
        val = self.num  
        self.num += 2  
        return val 

讓我們分解一下這段Python代碼:

 

  • __init __()方法是類構造函數,調用類時會首先執行該函數。它用於分配程序執行期間類最初所需的任何值。我在這裏設置num變量的初始值爲2;

  • iter()和next()方法使這個類變成了迭代器;

  • iter()方法返回迭代器對象並對迭代進行初始化。由於類對象本身是迭代器,因此它返回自身;

  • next()方法從迭代器中返回當前值,並改變下一次調用的狀態。我們將num變量的值加2,因爲我們只打印偶數。

我們可以創建Sequence對象來遍歷Sequence類,在該對象上調用next()方法:

it = Sequence()  
print(next(it))  
print(next(it))  
print(next(it))  
print(next(it))  
print(next(it)) 

 

我沒有寫sequence結束的條件,因此迭代器將永遠繼續返回下一個值。但我們可以使用停止條件輕鬆地對其進行更新:

class Sequence():  
    def __init__(self):  
        self.num = 2  
    def __iter__(self):  
        return self  
    def __next__(self):  
        val = self.num  
        if val>=10:  
            raise StopIteration  
        self.num += 2  
        return val    

我剛剛加入了一條if語句,只要值超過10,該語句就會停止迭代:

it = Sequence()  
for i in it:  
     print(i)

在這裏,我沒有使用next()方法從迭代器返回值,而是使用了for循環,該循環的工作方式與之前相同。

熟悉Python中的生成器

生成器也是迭代器,但更加優雅。使用生成器,我們可以實現與迭代器相同的功能,但不必在類中編寫iter()和next()函數。相反,我們可以使用一個簡單的函數來完成與迭代器相同的任務:

# fibonacci sequence using a generator   
def fib():     
    prev, curr = 0, 1  
    # infinite loop  
    while prev<5:  
        value = prev  
        # Calculate the next number in the sequence. Using Tuple unpacking.  
        prev, curr = curr, prev + curr  
        # yield the value  
        yield value  

你是否注意到這個生成器函數和常規函數的不同?是的,yield關鍵字!

 

普通函數使用return關鍵字返回值。但是生成器函數使用yield關鍵字返回值。這就是生成器函數與常規函數不同的地方(除了這種區別,它們是完全相同的)。

 

yield關鍵字的工作方式類似於普通的return關鍵字,但有額外的功能:它能記住函數的狀態。因此,下次調用generator函數時,它不是從頭開始,而是從上次調用中停止的位置開始。

 

讓我們看看它是如何工作的:

# generator object  
gen=fib()  
print(gen)  
# values  
print(next(gen))  
print(next(gen))  
print(next(gen))  
print(next(gen))  
print(next(gen))  

生成器屬於“生成器”類型,它是迭代器的一種特殊類型,但仍然是迭代器,因此它們也是懶惰的工作者。除非next()方法明確要求它們這樣做,否則它們不會返回任何值。

 

最初創建fib()生成器函數的對象時,它會初始化prev和curr變量。現在,當在對象上調用next()方法時,生成器函數會計算值並返回輸出,同時記住函數的狀態。因此,下次調用next()方法時,該函數將從上次停止的地方開始,從那裏繼續。

 

每當使用next()方法時,該函數將繼續生成值,直到prev變得大於5,這時將引起StopIteration異常,如下所示:

 

print(next(gen))

實現Python中的生成器表達式

 

你不必在每次執行生成器時都編寫函數。相反,你可以使用生成器表達式,就像列表生成式一樣。唯一的區別是,與列表生成式不同,生成器表達式包含在圓括號內,如下所示:

squared_gen = (x*x for x in range(2,5))  
print(squared_gen)

但它們仍然很懶,因此你需要使用next()方法。但你現在知道使用for循環可以更好地返回值:

for i in squared_gen:  
    print(i) 

當你編寫簡單的代碼時,生成器表達式非常有用,因爲它們易讀、易理解。但隨着代碼變得更復雜,它們的功能會迅速變弱。在這種情況下,你發現自己會重新使用生成器函數,生成器函數在編寫更復雜的函數方面提供了更大的靈活性。

爲什麼你應該使用迭代器?

一個重要的問題:爲什麼要先考慮用迭代器?

 

我在文章開頭提到了這一點:之所以使用迭代器,是因爲它們爲我們節省了大量內存。這是因爲迭代器在生成時不會計算項,而只會在調用它們時計算。

 

如果我創建一個包含1000萬個項的列表,並創建一個包含相同數量項的生成器,則它們內存大小上的差異將令人震驚:

 

import sys  
# list comprehension  
mylist = [i for i in range(10000000)]  
print('Size of list in memory',sys.getsizeof(mylist))  
# generator expression  
mygen = (i for i in range(10000000))  
print('Size of generator in memory',sys.getsizeof(mygen)

 

對於相同的數量的項,列表和生成器在內存大小上存在巨大差異。這就是迭代器的美。 

 

不僅如此,你可以使用迭代器逐行讀取文件中的文本,而不是一次性讀取所有內容。這會再次爲你節省大量內存,尤其是在文件很大的情況下。

 

在這裏,讓我們使用生成器來迭代讀取文件。爲此,我們可以創建一個簡單的生成器表達式來懶惰地打開文件,一次讀取一行:

 

file = "Greetings.txt"  
# generator expression  
lines = (line for line in open(file))  
print(lines)  
# print lines  
print(next(lines))  
print(next(lines))  
print(next(lines)) 

這很棒,但對於數據科學家或分析師而言,他們最終都要在Pandas的 dataframe中處理大型數據集。當你不得不處理龐大的數據集時,也許這個數據集有幾千行數據點甚至更多。如果Pandas可以解決這一難題,那麼數據科學家的生活將變得更加輕鬆。

 

好吧,你很幸運,因爲Pandas的read_csv()(https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_csv.html)有處理該問題的chunksize參數。它使你可以按指定大小的塊來加載數據,而不是將整個數據加載到內存中。處理完一個數據塊後,可以對dataframe對象執行next()方法來加載下一個數據塊。就這麼簡單!

 

我將讀取Black Friday數據集(https://datahack.analyticsvidhya.com/contest/black-friday/?utm_source=blog&utm_medium=python-iterators-and-generators),該數據集包含550,068行數據,讀取時設置每塊的大小爲10,這樣做只是爲了演示該函數的用法:

 

import pandas as pd  
  
# pandas dataframe  
df = pd.read_csv('./Black Friday.csv', chunksize=10)  
  
# print first chunk of data  
next(df)


# print second chunk of data  
next(df)  

 

 

很有用,不是嗎?

 

結語

我確信你現在已經習慣於使用迭代器,而且一定在考慮把所有函數轉換爲生成器!你開始喜歡Python編程的強大之處。

 

你以前使用過Python迭代器和生成器嗎?或者你要與社區分享其他“隱藏的寶石”?大家可以在下方評論!

 

原文標題:

What are Python Iterators and Generators? Programming Concepts Every Data Science Professional Should Know

原文鏈接:

https://www.analyticsvidhya.com/blog/2020/05/python-iterators-and-generators/ 

編輯:黃繼彥

校對:譚佳瑤

譯者簡介

王琦,中國科學院大學研一在讀,研究方向是機器學習與數據挖掘。喜歡探索新事物,是一個熱愛學習的人。 

翻譯組招募信息

工作內容:需要一顆細緻的心,將選取好的外文文章翻譯成流暢的中文。如果你是數據科學/統計學/計算機類的留學生,或在海外從事相關工作,或對自己外語水平有信心的朋友歡迎加入翻譯小組。

你能得到:定期的翻譯培訓提高志願者的翻譯水平,提高對於數據科學前沿的認知,海外的朋友可以和國內技術應用發展保持聯繫,THU數據派產學研的背景爲志願者帶來好的發展機遇。

其他福利:來自於名企的數據科學工作者,北大清華以及海外等名校學生他們都將成爲你在翻譯小組的夥伴。

點擊文末“閱讀原文”加入數據派團隊~

點擊“閱讀原文”擁抱組織

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