標籤:

python生成器到底有什麼優點?

例如:用[]推導出來的列表和()推導出來的生成器,在用for循環列印時,有什麼不同嗎?哪個環節不一樣?


在Python這門語言中,生成器毫無疑問是最有用的特性之一。與此同時,也是使用的最不廣泛的Python特性之一。究其原因,主要是因為,在其他主流語言裡面沒有生成器的概念。正是由於生成器是一個「新」的東西,所以,它一方面沒有引起廣大工程師的重視,另一方面,也增加了工程師的學習成本,最終導致大家錯過了Python中如此有用的一個特性。

我的這篇文章,希望通過簡單易懂的方式,深入淺出地介紹Python的生成器,以改變「如此有用的特性卻使用極不廣泛」的現象。本文的組織如下:在第1章,我們簡單地介紹了Python中的迭代器協議;在本文第2章,將會詳細介紹生成器的概念和語法;在第3章,將會給出一個有用的例子,說明使用生成器的好處;在本文最後,簡單的討論了使用生成器的注意事項。

1. 迭代器協議

由於生成器自動實現了迭代器協議,而迭代器協議對很多人來說,也是一個較為抽象的概念。所以,為了更好的理解生成器,我們需要簡單的回顧一下迭代器協議的概念。

  1. 迭代器協議是指:對象需要提供next方法,它要麼返回迭代中的下一項,要麼就引起一個StopIteration異常,以終止迭代

  2. 可迭代對象就是:實現了迭代器協議的對象

  3. 協議是一種約定,可迭代對象實現迭代器協議,Python的內置工具(如for循環,sum,min,max函數等)使用迭代器協議訪問對象。

舉個例子:在所有語言中,我們都可以使用for循環來遍曆數組,Python的list底層實現是一個數組,所以,我們可以使用for循環來遍歷list。如下所示:

&>&>&> for n in [1, 2, 3, 4]:
... print n

但是,對Python稍微熟悉一點的朋友應該知道,Python的for循環不但可以用來遍歷list,還可以用來遍歷文件對象,如下所示:

&>&>&> with open(『/etc/passwd』) as f: # 文件對象提供迭代器協議
... for line in f: # for循環使用迭代器協議訪問文件
... print line
...

為什麼在Python中,文件還可以使用for循環進行遍歷呢?這是因為,在Python中,文件對象實現了迭代器協議,for循環並不知道它遍歷的是一個文件對象,它只管使用迭代器協議訪問對象即可。正是由於Python的文件對象實現了迭代器協議,我們才得以使用如此方便的方式訪問文件,如下所示:

&>&>&> f = open("/etc/passwd")
&>&>&> dir(f)
["__class__", "__enter__", "__exit__", "__iter__", "__new__", "writelines", "..."

2. 生成器

Python使用生成器對延遲操作提供了支持。所謂延遲操作,是指在需要的時候才產生結果,而不是立即產生結果。這也是生成器的主要好處。

Python有兩種不同的方式提供生成器:

  1. 生成器函數:常規函數定義,但是,使用yield語句而不是return語句返回結果。yield語句一次返回一個結果,在每個結果中間,掛起函數的狀態,以便下次重它離開的地方繼續執行
  2. 生成器表達式:類似於列表推導,但是,生成器返回按需產生結果的一個對象,而不是一次構建一個結果列表

2.1 生成器函數

我們來看一個例子,使用生成器返回自然數的平方(注意返回的是多個值):

def gensquares(N):
for i in range(N):
yield i ** 2

for item in gensquares(5):
print item,

使用普通函數:

def gensquares(N):
res = []
for i in range(N):
res.append(i*i)
return res

for item in gensquares(5):
print item,

可以看到,使用生成器函數代碼量更少。

2.2 生成器表達式

使用列表推導,將會一次產生所有結果:

&>&>&> squares = [x**2 for x in range(5)]
&>&>&> squares
[0, 1, 4, 9, 16]

將列表推導的中括弧,替換成圓括弧,就是一個生成器表達式:

&>&>&> squares = (x**2 for x in range(5))
&>&>&> squares
&
&>&>&> next(squares)
0
&>&>&> next(squares)
1
&>&>&> next(squares)
4
&>&>&> list(squares)
[9, 16]

Python不但使用迭代器協議,讓for循環變得更加通用。大部分內置函數,也是使用迭代器協議訪問對象的。例如, sum函數是Python的內置函數,該函數使用迭代器協議訪問對象,而生成器實現了迭代器協議,所以,我們可以直接這樣計算一系列值的和:

&>&>&> sum(x ** 2 for x in xrange(4))

而不用多此一舉的先構造一個列表:

&>&>&> sum([x ** 2 for x in xrange(4)])

2.3 再看生成器

前面已經對生成器有了感性的認識,我們以生成器函數為例,再來深入探討一下Python的生成器:

  1. 語法上和函數類似:生成器函數和常規函數幾乎是一樣的。它們都是使用def語句進行定義,差別在於,生成器使用yield語句返回一個值,而常規函數使用return語句返回一個值
  2. 自動實現迭代器協議:對於生成器,Python會自動實現迭代器協議,以便應用到迭代背景中(如for循環,sum函數)。由於生成器自動實現了迭代器協議,所以,我們可以調用它的next方法,並且,在沒有值可以返回的時候,生成器自動產生StopIteration異常
  3. 狀態掛起:生成器使用yield語句返回一個值。yield語句掛起該生成器函數的狀態,保留足夠的信息,以便之後從它離開的地方繼續執行

3. 示例

我們再來看兩個生成器的例子,以便大家更好的理解生成器的作用。

首先,生成器的好處是延遲計算,一次返回一個結果。也就是說,它不會一次生成所有的結果,這對於大數據量處理,將會非常有用。

大家可以在自己電腦上試試下面兩個表達式,並且觀察內存佔用情況。對於前一個表達式,我在自己的電腦上進行測試,還沒有看到最終結果電腦就已經卡死,對於後一個表達式,幾乎沒有什麼內存佔用。

sum([i for i in xrange(10000000000)])
sum(i for i in xrange(10000000000))

除了延遲計算,生成器還能有效提高代碼可讀性。例如,現在有一個需求,求一段文字中,每個單詞出現的位置。

不使用生成器的情況:

def index_words(text):
result = []
if text:
result.append(0)
for index, letter in enumerate(text, 1):
if letter == " ":
result.append(index)
return result

使用生成器的情況:

def index_words(text):
if text:
yield 0
for index, letter in enumerate(text, 1):
if letter == " ":
yield index

這裡,至少有兩個充分的理由說明 ,使用生成器比不使用生成器代碼更加清晰:

  1. 使用生成器以後,代碼行數更少。大家要記住,如果想把代碼寫的Pythonic,在保證代碼可讀性的前提下,代碼行數越少越好
  2. 不使用生成器的時候,對於每次結果,我們首先看到的是result.append(index),其次,才是index。也就是說,我們每次看到的是一個列表的append操作,只是append的是我們想要的結果。使用生成器的時候,直接yield index,少了列表append操作的干擾,我們一眼就能夠看出,代碼是要返回index。

這個例子充分說明了,合理使用生成器,能夠有效提高代碼可讀性。只要大家完全接受了生成器的概念,理解了yield語句和return語句一樣,也是返回一個值。那麼,就能夠理解為什麼使用生成器比不使用生成器要好,能夠理解使用生成器真的可以讓代碼變得清晰易懂。

4. 使用生成器的注意事項

相信通過這篇文章,大家已經能夠理解生成器的作用和好處。但是,還沒有結束,使用生成器,也有一點注意事項。

我們直接來看例子,假設文件中保存了每個省份的人口總數,現在,需要求每個省份的人口佔全國總人口的比例。顯然,我們需要先求出全國的總人口,然後在遍歷每個省份的人口,用每個省的人口數除以總人口數,就得到了每個省份的人口佔全國人口的比例。

如下所示:

def get_province_population(filename):
with open(filename) as f:
for line in f:
yield int(line)

gen = get_province_population("data.txt")
all_population = sum(gen)
#print all_population
for population in gen:
print population / all_population

執行上面這段代碼,將不會有任何輸出,這是因為,生成器只能遍歷一次。在我們執行sum語句的時候,就遍歷了我們的生成器,當我們再次遍歷我們的生成器的時候,將不會有任何記錄。所以,上面的代碼不會有任何輸出。

因此,生成器的唯一注意事項就是:生成器只能遍歷一次

5. 總結

本文深入淺出地介紹了Python中,一個容易被大家忽略的重要特性,即Python的生成器。為了講解生成器,本文先介紹了迭代器協議,然後介紹了生成器函數和生成器表達式,並通過示例演示了生成器的優點和注意事項。在實際工作中,充分利用Python生成器,不但能夠減少內存使用,還能夠提高代碼可讀性。掌握生成器也是Python高手的標配。希望本文能夠幫助大家理解Python的生成器。

對我這篇文章感興趣的同學,也可以看看我之前的回答:怎麼樣才算是精通 Python?


[]是通過遍歷可迭代對象生成一個list

()是直接返回可迭代對象


Lazy Evaluation


比如你要一個無限長的列表的時候,就有用了


用[]推導出來的列表和()推導出來的生成器有什麼不同?

先回答一下這個。

首先,用[]推導出來的是迭代器(Iterables)。用()推導出來的是生成器(Generators)。

簡單代碼如下

&>&>&> mylist = [x*x for x in range(3)]
&>&>&> for i in mylist:
... print(i)
0
1
4

其實,任何東西, for value in something,這個something就是一個iterable。像list,string,file。同時,你需要注意,你是把所有的東西都存在內存中的。但是當你的iterable很大,例如這個list有一千萬個元素的時候,就不是一個很令人開心的事情了。

然後,類似的代碼如下

&>&>&> mygenerator = (x*x for x in range(3))
&>&>&> for i in mygenerator:
... print(i)
0
1
4

你會發現,除了中括弧變成小括弧之外,並沒有什麼區別,是不是?

這個就是生成器,generator,其實從嚴格意義上來說,generator也是iterators。但是你只能迭代他們一次。因為他們不會吧所有的值都存在內存中。

&>&>&> for i in mygenerator:
... print(i)

就是這段代碼,當你再跑一遍的時候,你會發現就沒有輸出了。而迭代器不一樣,你執行多少次都是會有結果的。

這個就是,先計算了0,然後就忘掉。計算了1,然後又忘掉。依次類推。

現在,你知道用()和[]推導出來的有什麼區別了吧?

然後,yield其實從某個角度來看,就是一個return。不過它返回的就是一個generator。好了,就先到這裡。等下有時間繼續補充。

不過,其實到這裡,你已經差不多該懂了,再臨時加兩行代碼,看完估計你就明白了。但是往深處說,這個又有很多地方可以玩的。

&>&>&> def createGenerator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
&>&>&> mygenerator = createGenerator() # create a generator
&>&>&> print(mygenerator) # mygenerator is an object!
&
&>&>&> for i in mygenerator:
... print(i)
0
1
4


這怎麼會扯上惰性求值了,生成器的原型不是continuation么。

當然生成器只是continuation的一個簡單的應用,continuation能幹的事情多著呢,比如最近比較流行的coroutine


生成器是可迭代對象,是惰性求值的。列表推導式產生的是一個列表,結果已經存在於內存之中了。《python學習手冊》中對生成器有很詳細的描述,條件允許可以去看看。


class A(object):
def __init__(self, i):
from time import sleep, time
sleep(i)
print (time())

[A(i) for i in range(5)]
(A(i) for i in range(5))

試試就知道區別。道理就是 @Twisted Python 說的那兩個字。


最近正在學python,感覺這語言不錯,之前是做PHP的,現在自學了半個月了!以我自己對生成器的理解,能說的就以下3點,如果不對還請高手指正,畢竟現在還是個python小白:

1、節省資源消耗,和聲明序列不同的是生成器在不使用的時候幾乎不佔內存,也沒有聲明計算過程!

2、使用的時候,生成器是隨用隨生成,用完即刻釋放,非常高效!

3、可在單線程下實現並發運算處理效果,非常牛逼,這點不可小視,看看nginx epoll單線程承載的並發量比多線程還效率高很多,最底層就是這個原理!


python生成器優點?

省內存。如果不需要一次遍歷所有結果,生成器的惰性求值(Lazy evaluation - Wikipedia)更好。

例如對於大文件內搜索關鍵詞,generator 和 yield 的組合更靈活:

def search(keyword, filename):
# Temporarily read the file
f = open(filename, "r")
# Loop through the file by line
for line in f:
# Return the line with keyword
if keyword in line:
yield line
f.close()

列表和生成器的不同?哪個環節?

不同的數據類型,List type 屬於 Sequence Type;Generator Type 屬於 Iteration Type。

生成器沒有長度。


推薦閱讀:

金融工程現在用python多嗎?
dlib人臉識別 模塊 如何 在Python 中安裝?
如何把嵌套的python list轉成一個一維的python list?
為什麼說 Python 是強類型語言?
C/C++ 這類更底層的語言,如果把平時常用的高級函數和功能都實現,能否達到 Python 的開發效率?

TAG:Python |