首頁 >後端開發 >Python教學 >詳解yield和Generators生成器

詳解yield和Generators生成器

巴扎黑
巴扎黑原創
2017-09-19 10:34:461795瀏覽

產生器和yield關鍵字可能是Python裡面最強大的最難理解的概念之一(或許沒有之一), 但是並不妨礙yield成為Python裡面最強大的關鍵字,對於初學者來講確實非常難於理解,來看一篇關於yield的國外大牛寫的文章,讓你快速理解yield。 文章有點長,請耐心讀完, 過程中有些例子, 循序漸進,讓你不覺得枯燥。

生成器

生成器是透過一個或多個yield表達式構成的函數,每一個生成器都是一個迭代器(但是迭代器不一定是生成器)。

如果一個函數包含yield關鍵字,這個函數就會變成一個生成器。

產生器不會一次傳回所有結果,而是每次遇到yield關鍵字後傳回對應結果,並保留函數目前的運行狀態,等待下一次的呼叫。

由於生成器也是迭代器,那麼它就應該支援next方法來取得下一個值。 (也可以使用.__next__()屬性, 在python2 中是.next()

協程與子程式

我們呼叫一個普通的Python函數時,一般是從函數的第一行程式碼開始執行,結束於return語句、異常或函數結束(可以看作隱含的回傳None)。一旦函數將控制權交還給呼叫者,就意味著全部結束。函數中所做的所有工作以及保存在局部變數中的資料都會遺失。再次呼叫這個函數時,一切都會從頭開始建立。 

對於在電腦程式設計中所討論的函數,這是一個很標準的流程。這樣的函數只能回傳一個值,不過,有時可以建立能產生一個序列的函數還是有幫助的。要做到這一點,這種函數需要能夠「保存自己的工作」。 

我說過,能夠「產生一個序列」是因為我們的函數並沒有像通常意義那樣回傳。 return隱含的意思是函數正將執行程式碼的控制權回傳給函數被呼叫的地方。而"yield"的隱含意思是控制權的轉移是暫時和自願的,我們的函數將來還會收回控制權。

在Python中,擁有這種能力的「函數」稱為生成器,它非常有用的。生成器(以及yield語句)最初的引入是為了讓程式設計師可以更簡單的編寫用來產生值的序列的程式碼。 以前,要實現類似隨機數產生器的東西,需要實作一個類別或模組,在產生資料的同時保持對每次呼叫之間狀態的追蹤。引入生成器之後,這變得非常簡單。

為了更好的理解生成器所解決的問題,讓我們來看一個範例。在了解這個例子的過程中,請始終記住我們需要解決的問題:產生值的序列。

注意:在Python之外,最簡單的生成器應該是被稱為協程(coroutines)的東西。在本文中,我將使用這個術語。請記住,在Python的概念中,這裡提到的協程就是生成器。 Python正式的術語是生成器;協程只是便於討論,在語言層面並沒有正式定義。

範例:有趣的質數

假設你的老闆要你寫一個函數,輸入參數是一個int的list,回傳一個可以迭代的包含素數1 的結果。

記住,迭代器(Iterable) 只是物件每次回傳特定成員的一種能力。

你肯定認為"這很簡單",然後很快寫出下面的程式碼:

def get_primes(input_list):
    result_list = list()
    for element in input_list:
        if is_prime(element):
            result_list.append()
    return result_list
# 或者更好一些的...
def get_primes(input_list):
    return (element for element in input_list if is_prime(element))
# 下面是 is_prime 的一种实现...
def is_prime(number):
    if number > 1:
        if number == 2:
            return True
        if number % 2 == 0:
            return False
        for current in range(3, int(math.sqrt(number) + 1), 2):
            if number % current == 0: 
                return False
        return True
    return False

上面 is_prime 的實作完全滿足了需求,所以我們告訴老闆已經搞定了。她回饋說我們的函數工作正常,正是她想要的。

處理無限序列

噢,真是如此嗎?過了幾天,老闆過來告訴我們她遇到了一些小問題:她打算把我們的get_primes函數用在一個很大的包含數字的list。實際上,這個list非常大,光是創建這個list就會用完系統的所有記憶體。為此,她希望能夠在呼叫get_primes函數時帶上一個start參數,並傳回所有大於這個參數的質數(也許她要解決 Project Euler problem 10)。

我們來看看這個新需求,很明顯只是簡單的修改get_primes是不可能的。 自然,我們不可能傳回包含從start到無窮的所有的質數的列表 (雖然有很多有用的應用程式可以用來操作無限序列)。看起來用普通函數處理這個問題的可能性比較渺茫。

在我們放棄之前,讓我們先確定一下最核心的障礙,是什麼阻止我們編寫滿足老闆新需求的函數。透過思考,我們得到這樣的結論:函數只有一次回傳結果的機會,因而必須一次回傳所有的結果。得出這樣的結論似乎毫無意義;“函數不就是這樣工作的麼”,通常我們都這麼認為的。可是,不學不成,不問不知,「如果它們並非如此呢?」

想象一下,如果get_primes可以只是简单返回下一个值,而不是一次返回全部的值,我们能做什么?我们就不再需要创建列表。没有列表,就没有内存的问题。由于老板告诉我们的是,她只需要遍历结果,她不会知道我们实现上的区别。

不幸的是,这样做看上去似乎不太可能。即使是我们有神奇的函数,可以让我们从n遍历到无限大,我们也会在返回第一个值之后卡住:

def get_primes(start):
    for element in magical_infinite_range(start):
        if is_prime(element):
            return element

假设这样去调用get_primes:

def solve_number_10():
    # She *is* working on Project Euler #10, I knew it!
    total = 2
    for next_prime in get_primes(3):
        if next_prime < 2000000:
            total += next_prime
        else:
            print(total)
            return

显然,在get_primes中,一上来就会碰到输入等于3的,并且在函数的第4行返回。与直接返回不同,我们需要的是在退出时可以为下一次请求准备一个值。

不过函数做不到这一点。当函数返回时,意味着全部完成。我们保证函数可以再次被调用,但是我们没法保证说,“呃,这次从上次退出时的第4行开始执行,而不是常规的从第一行开始”。函数只有一个单一的入口:函数的第1行代码。

走进生成器

这类问题极其常见以至于Python专门加入了一个结构来解决它:生成器。一个生成器会“生成”值。创建一个生成器几乎和生成器函数的原理一样简单。

一个生成器函数的定义很像一个普通的函数,除了当它要生成一个值的时候,使用yield关键字而不是return。如果一个def的主体包含yield,这个函数会自动变成一个生成器(即使它包含一个return)。除了以上内容,创建一个生成器没有什么多余步骤了。

生成器函数返回生成器的迭代器。这可能是你最后一次见到“生成器的迭代器”这个术语了, 因为它们通常就被称作“生成器”。要注意的是生成器就是一类特殊的迭代器。作为一个迭代器,生成器必须要定义一些方法(method),其中一个就是__next__()【注意: 在python2中是: next() 方法】。如同迭代器一样,我们可以使用next()函数来获取下一个值。

为了从生成器获取下一个值,我们使用next()函数,就像对付迭代器一样。

(next()会操心如何调用生成器的__next__()方法)。既然生成器是一个迭代器,它可以被用在for循环中。

每当生成器被调用的时候,它会返回一个值给调用者。在生成器内部使用yield来完成这个动作(例如yield 7)。为了记住yield到底干了什么,最简单的方法是把它当作专门给生成器函数用的特殊的return(加上点小魔法)。**

yield就是专门给生成器用的return(加上点小魔法)。

下面是一个简单的生成器函数:

>>> def simple_generator_function():
>>>    yield 1
>>>    yield 2
>>>    yield 3

这里有两个简单的方法来使用它:

>>> for value in simple_generator_function():
>>>     print(value)
1
2
3
>>> our_generator = simple_generator_function()
>>> next(our_generator)
1
>>> next(our_generator)
2
>>> next(our_generator)
3

魔法?

那么神奇的部分在哪里?我很高兴你问了这个问题!当一个生成器函数调用yield,生成器函数的“状态”会被冻结,所有的变量的值会被保留下来,下一行要执行的代码的位置也会被记录,直到再次调用next()。一旦next()再次被调用,生成器函数会从它上次离开的地方开始。如果永远不调用next(),yield保存的状态就被无视了。

我们来重写get_primes()函数,这次我们把它写作一个生成器。注意我们不再需要magical_infinite_range函数了。使用一个简单的while循环,我们创造了自己的无穷串列。

def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1

如果生成器函数调用了return,或者执行到函数的末尾,会出现一个StopIteration异常。 这会通知next()的调用者这个生成器没有下一个值了(这就是普通迭代器的行为)。这也是这个while循环在我们的get_primes()函数出现的原因。如果没有这个while,当我们第二次调用next()的时候,生成器函数会执行到函数末尾,触发StopIteration异常。一旦生成器的值用完了,再调用next()就会出现错误,所以你只能将每个生成器的使用一次。下面的代码是错误的:

>>> our_generator = simple_generator_function()
>>> for value in our_generator:
>>>     print(value)
>>> # 我们的生成器没有下一个值了...
>>> print(next(our_generator))
Traceback (most recent call last):
  File "<ipython-input-13-7e48a609051a>", line 1, in <module>
    next(our_generator)
StopIteration
>>> # 然而,我们总可以再创建一个生成器
>>> # 只需再次调用生成器函数即可
>>> new_generator = simple_generator_function()
>>> print(next(new_generator)) # 工作正常
1

因此,这个while循环是用来确保生成器函数永远也不会执行到函数末尾的。只要调用next()这个生成器就会生成一个值。这是一个处理无穷序列的常见方法(这类生成器也是很常见的)。

执行流程

让我们回到调用get_primes的地方:solve_number_10。

def solve_number_10():
    # She *is* working on Project Euler #10, I knew it!
    total = 2
    for next_prime in get_primes(3):
        if next_prime < 2000000:
            total += next_prime
        else:
            print(total)
            return

我们来看一下solve_number_10的for循环中对get_primes的调用,观察一下前几个元素是如何创建的有助于我们的理解。当for循环从get_primes请求第一个值时,我们进入get_primes,这时与进入普通函数没有区别。

进入第三行的while循环

停在if条件判断(3是素数)

通过yield将3和执行控制权返回给solve_number_10

接下来,回到insolve_number_10:

for循环得到返回值3

for循环将其赋给next_prime

total加上next_prime

for循环从get_primes请求下一个值

这次,进入get_primes时并没有从开头执行,我们从第5行继续执行,也就是上次离开的地方。

def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1 # <<<<<<<<<<

最关键的是,number还保持我们上次调用yield时的值(例如3)。记住,yield会将值传给next()的调用方,同时还会保存生成器函数的“状态”。接下来,number加到4,回到while循环的开始处,然后继续增加直到得到下一个素数(5)。我们再一次把number的值通过yield返回给solve_number_10的for循环。这个周期会一直执行,直到for循环结束(得到的素数大于2,000,000)。

总结

关键点:

generator是用来产生一系列值的

yield则像是generator函数的返回结果

yield唯一所做的另一件事就是保存一个generator函数的状态

generator就是一个特殊类型的迭代器(iterator)

和迭代器相似,我们可以通过使用next()来从generator中获取下一个值

通过隐式地调用next()来忽略一些值

以上是詳解yield和Generators生成器的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn