生成器

2018-02-24 15:48 更新

生成器(英文:generator)是一個非常迷人的東西,也常被認為是python的高級編程技能。不過,我依然很樂意在這里跟讀者——盡管你可能是一個初學者——探討這個話題,因為我相信讀者看本教程的目的,絕非僅僅將自己限制于初學者水平,一定有一顆不羈的心——要成為python高手。那么,開始了解生成器吧。

還記得上節(jié)的“迭代器”嗎?生成器和迭代器有著一定的淵源關系。生成器必須是可迭代的,誠然它又不僅僅是迭代器,但除此之外,又沒有太多的別的用途,所以,我們可以把它理解為非常方便的自定義迭代器。

最這個關系實在感覺有點糊涂了。稍安勿躁,繼續(xù)閱讀即明了。

簡單的生成器

>>> my_generator = (x*x for x in range(4))

這是不是跟列表解析很類似呢?仔細觀察,它不是列表,如果這樣的得到的才是列表:

>>> my_list = [x*x for x in range(4)]

以上兩的區(qū)別在于是[]還是(),雖然是細小的差別,但是結果完全不一樣。

>>> dir(my_generator)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', 
'__iter__', 
'__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 
'next', 
'send', 'throw']

為了容易觀察,我將上述結果進行了重新排版。是不是發(fā)現了在迭代器中必有的方法__inter__()next(),這說明它是迭代器。如果是迭代器,就可以用for循環(huán)來依次讀出其值。

>>> for i in my_generator:
...     print i
... 
0
1
4
9
>>> for i in my_generator:
...     print i
... 

當第一遍循環(huán)的時候,將my_generator里面的值依次讀出并打印,但是,當再讀一次的時候,就發(fā)現沒有任何結果。這種特性也正是迭代器所具有的。

如果對那個列表,就不一樣了:

>>> for i in my_list:
...     print i
... 
0
1
4
9
>>> for i in my_list:
...     print i
... 
0
1
4
9

難道生成器就是把列表解析中的[]換成()就行了嗎?這僅僅是生成器的一種表現形式和使用方法罷了,仿照列表解析式的命名,可以稱之為“生成器解析式”(或者:生成器推導式、生成器表達式)。

生成器解析式是有很多用途的,在不少地方替代列表,是一個不錯的選擇。特別是針對大量值的時候,如上節(jié)所說的,列表占內存較多,迭代器(生成器是迭代器)的優(yōu)勢就在于少占內存,因此無需將生成器(或者說是迭代器)實例化為一個列表,直接對其進行操作,方顯示出其迭代的優(yōu)勢。比如:

>>> sum(i*i for i in range(10))
285

請讀者注意觀察上面的sum()運算,不要以為里面少了一個括號,就是這么寫。是不是很迷人?如果列表,你不得不:

>>> sum([i*i for i in range(10)])
285

通過生成器解析式得到的生成器,掩蓋了生成器的一些細節(jié),并且適用領域也有限。下面就要剖析生成器的內部,深入理解這個魔法工具。

定義和執(zhí)行過程

yield這個詞在漢語中有“生產、出產”之意,在python中,它作為一個關鍵詞(你在變量、函數、類的名稱中就不能用這個了),是生成器的標志。

>>> def g():
...     yield 0
...     yield 1
...     yield 2
... 
>>> g
<function g at 0xb71f3b8c>

建立了一個非常簡單的函數,跟以往看到的函數唯一不同的地方是用了三個yield語句。然后進行下面的操作:

>>> ge = g()
>>> ge
<generator object g at 0xb7200edc>
>>> type(ge)
<type 'generator'>

上面建立的函數返回值是一個生成器(generator)類型的對象。

>>> dir(ge)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'next', 'send', 'throw']

在這里看到了__iter__()next(),說明它是迭代器。既然如此,當然可以:

>>> ge.next()
0
>>> ge.next()
1
>>> ge.next()
2
>>> ge.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

從這個簡單例子中可以看出,那個含有yield關鍵詞的函數返回值是一個生成器類型的對象,這個生成器對象就是迭代器。

我們把含有yield語句的函數稱作生成器。生成器是一種用普通函數語法定義的迭代器。通過上面的例子可以看出,這個生成器(也是迭代器),在定義過程中并沒有像上節(jié)迭代器那樣寫__inter__()next(),而是只要用了yield語句,那個普通函數就神奇般地成為了生成器,也就具備了迭代器的功能特性。

yield語句的作用,就是在調用的時候返回相應的值。詳細剖析一下上面的運行過程:

  1. ge = g():除了返回生成器之外,什么也沒有操作,任何值也沒有被返回。
  2. ge.next():直到這時候,生成器才開始執(zhí)行,遇到了第一個yield語句,將值返回,并暫停執(zhí)行(有的稱之為掛起)。
  3. ge.next():從上次暫停的位置開始,繼續(xù)向下執(zhí)行,遇到y(tǒng)ield語句,將值返回,又暫停。
  4. gen.next():重復上面的操作。
  5. gene.next():從上面的掛起位置開始,但是后面沒有可執(zhí)行的了,于是next()發(fā)出異常。

從上面的執(zhí)行過程中,發(fā)現yield除了作為生成器的標志之外,還有一個功能就是返回值。那么它跟return這個返回值有什么區(qū)別呢?

yield

為了弄清楚yield和return的區(qū)別,我們寫兩個沒有什么用途的函數:

>>> def r_return(n):
...     print "You taked me."
...     while n > 0:
...         print "before return"
...         return n
...         n -= 1
...         print "after return"
... 
>>> rr = r_return(3)
You taked me.
before return
>>> rr
3

從函數被調用的過程可以清晰看出,rr = r_return(3),函數體內的語句就開始執(zhí)行了,遇到return,將值返回,然后就結束函數體內的執(zhí)行。所以return后面的語句根本沒有執(zhí)行。這是return的特點,關于此特點的詳細說明請閱讀《函數(2)》中的返回值相關內容。

下面將return改為yield:

>>> def y_yield(n):
...     print "You taked me."
...     while n > 0:
...         print "before yield"
...         yield n
...         n -= 1
...         print "after yield"
... 
>>> yy = y_yield(3)    #沒有執(zhí)行函數體內語句
>>> yy.next()          #開始執(zhí)行
You taked me.
before yield
3                      #遇到y(tǒng)ield,返回值,并暫停
>>> yy.next()          #從上次暫停位置開始繼續(xù)執(zhí)行
after yield
before yield
2                      #又遇到y(tǒng)ield,返回值,并暫停
>>> yy.next()          #重復上述過程
after yield
before yield
1
>>> yy.next()
after yield            #沒有滿足條件的值,拋出異常
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

結合注釋和前面對執(zhí)行過程的分析,讀者一定能理解yield的特點了,也深知與return的區(qū)別了。

一般的函數,都是止于return。作為生成器的函數,由于有了yield,則會遇到它掛起,如果還有return,遇到它就直接拋出SoptIteration異常而中止迭代。

斐波那契數列已經是老相識了。不論是循環(huán)、迭代都用它舉例過,現在讓我們還用它吧,只不過是要用上yield:

#!/usr/bin/env python
# coding=utf-8

def fibs(max):
    """
    斐波那契數列的生成器
    """
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1

if __name__ == "__main__":
    f = fibs(10)
    for i in f:
        print i ,

運行結果如下:

$ python 21501.py
1 1 2 3 5 8 13 21 34 55

用生成器方式實現的斐波那契數列是不是跟以前的有所不同了呢?讀者可以將本教程中已經演示過的斐波那契數列實現方式做一下對比,體會各種方法的差異。

經過上面的各種例子,已經明確,一個函數中,只要包含了yield語句,它就是生成器,也是迭代器。這種方式顯然比前面寫迭代器的類要簡便多了。但,并不意味著上節(jié)的就被拋棄。是生成器還是迭代器,都是根據具體的使用情景而定。

生成器方法

在python2.5以后,生成器有了一個新特征,就是在開始運行后能夠為生成器提供新的值。這就好似生成器和“外界”之間進行數據交流。

>>> def repeater(n):
...     while True:
...         n = (yield n)
... 
>>> r = repeater(4)
>>> r.next()
4
>>> r.send("hello")
'hello'

當執(zhí)行到r.next()的時候,生成器開始執(zhí)行,在內部遇到了yield n掛起。注意在生成器函數中,n = (yield n)中的yield n是一個表達式,并將結果賦值給n,雖然不嚴格要求它必須用圓括號包裹,但是一般情況都這么做,請讀者也追隨這個習慣。

當執(zhí)行r.send("hello")的時候,原來已經被掛起的生成器(函數)又被喚醒,開始執(zhí)行n = (yield n),也就是講send()方法發(fā)送的值返回。這就是在運行后能夠為生成器提供值的含義。

如果接下來再執(zhí)行r.next()會怎樣?

>>> r.next()

什么也沒有,其實就是返回了None。按照前面的敘述,讀者可以看到,這次執(zhí)行r.next(),由于沒有傳入任何值,yield返回的就只能是None.

還要注意,send()方法必須在生成器運行后并掛起才能使用,也就是yield至少被執(zhí)行一次。如果不是這樣:

>>> s = repeater(5)
>>> s.send("how")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator

就報錯了。但是,可將參數設為None:

>>> s.send(None)
5

這是返回的是調用函數的時傳入的值。

此外,還有兩個方法:close()和throw()

  • throw(type, value=None, traceback=None):用于在生成器內部(生成器的當前掛起處,或未啟動時在定義處)拋出一個異常(在yield表達式中)。
  • close():調用時不用參數,用于關閉生成器。

最后一句,你在編程中,不用生成器也可以。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號