生成器和迭代器

迭代器是訪問集合元素的一種方式。迭代器對象從集合的第一個元素開始訪問,直到所有的元素被訪問完結束。迭代器只能往前不會後退,不過這也沒什麼,因爲人們很少在迭代途中往後退。

使用迭代器的優點

對於原生支持隨機訪問的數據結構(如tuple、list),迭代器和經典for循環的索引訪問相比並無優勢,反而丟失了索引值(可以使用內建函數enumerate()找回這個索引值)。但對於無法隨機訪問的數據結構(比如set)而言,迭代器是唯一的訪問元素的方式。

另外,迭代器的一大優點是不要求事先準備好整個迭代過程中所有的元素。迭代器僅僅在迭代到某個元素時才計算該元素,而在這之前或之後,元素可以不存在或者被銷燬。這個特點使得它特別適合用於遍歷一些巨大的或是無限的集合,比如幾個G的文件,或是斐波那契數列等等。

迭代器更大的功勞是提供了一個統一的訪問集合的接口,只要定義了__iter__()方法對象,就可以使用迭代器訪問。
 
迭代器有兩個基本的方法
next方法:返回迭代器的下一個元素
__iter__方法:返回迭代器對象本身

一、迭代器Iterators
迭代器僅是一容器對象,它實現了迭代器協議。它有兩個基本方法:
1)next方法
返回容器的下一個元素
2)__iter__方法
返回迭代器自身

迭代器可使用內建的iter方法創建,見例子:
>>> i = iter('abc')
>>> i.next()
'a'
>>> i.next()
'b'
>>> i.next()
'c'
>>> i.next()
Traceback (most recent call last):
  File "<string>", line 1, in <string>
StopIteration:

class MyIterator(object):
  def __init__(self, step):
  self.step = step
  def next(self):
  """Returns the next element."""
  if self.step==0:
  raise StopIteration
  self.step-=1
  return self.step
  def __iter__(self):
  """Returns the iterator itself."""
  return self
for el in MyIterator(4):
  print el
--------------------
結果:
3
2
1
0

二、生成器Generators
從Python2.2起,生成器提供了一種簡潔的方式幫助返回列表元素的函數來完成簡單和有效的代碼。
它基於yield指令,允許停止函數並立即返回結果。
此函數保存其執行上下文,如果需要,可立即繼續執行。
例如Fibonacci函數:
def fibonacci():
  a,b=0,1
  while True:
  yield b
  a,b = b, a+b
fib=fibonacci()
print fib.next()
print fib.next()
print fib.next()
print [fib.next() for i in range(10)]
--------------------
結果:
1
1
2
[3, 5, 8, 13, 21, 34, 55, 89, 144, 233]

PEP Python Enhancement Proposal Python增強建議

tokenize模塊
>>> import tokenize
>>> reader = open('c:/temp/py1.py').next
>>> tokens=tokenize.generate_tokens(reader)
>>> tokens.next()
(1, 'class', (1, 0), (1, 5), 'class MyIterator(object):/n')
>>> tokens.next()
(1, 'MyIterator', (1, 6), (1, 16), 'class MyIterator(object):/n')
>>> tokens.next()
(51, '(', (1, 16), (1, 17), 'class MyIterator(object):/n')

例子:
def power(values):
  for value in values:
  print 'powering %s' %value
  yield value
def adder(values):
  for value in values:
  print 'adding to %s' %value
  if value%2==0:
  yield value+3
  else:
  yield value+2
elements = [1,4,7,9,12,19]
res = adder(power(elements))
print res.next()
print res.next()
--------------------
結果:
powering 1
adding to 1
3
powering 4
adding to 4
7

保持代碼簡單,而不是數據。
注意:寧可有大量簡單的可迭代函數,也不要一個複雜的一次只計算出一個值的函數。

例子:
def psychologist():
  print 'Please tell me your problems'
  while True:
  answer = (yield)
  if answer is not None:
  if answer.endswith('?'):
  print ("Don't ask yourself too much questions")
  elif 'good' in answer:
  print "A that's good, go on"
  elif 'bad' in answer:
  print "Don't be so negative"
free = psychologist()
print free.next()
print free.send('I feel bad')
print free.send("Why I shouldn't ?")
print free.send("ok then i should find what is good for me")
--------------------
結果:
Please tell me your problems
None
Don't be so negative
None
Don't ask yourself too much questions
None
A that's good, go on
None

雖然很早之前就接觸yield這個詞了,卻一直是一知半解。趁現在有時間,把它研究一通再說。

含有yield的函數說明它是一個生成器,而不是普通的函數。當程序運行到yield這一行時,該函數會返回值,並保存當前域的所有變量狀態;等到該函數下一次被調用時,會從上一次中斷的地方開始執行,一直遇到下一個yield, 程序返回值, 並在此保存當前狀態; 如此反覆,直到函數正常執行完成。

我一開始還想不明白調用者與生成器之間的函數堆棧是怎麼做到的,後來才大悟原來是用到了'協程'這個原理。協程可視爲微線程,下面會結合例子來說明一下yield及協程的運行過程。假設定義了test方法:

[python]

def test(len):  

    i = 0  

    while i < len :  

        yield i  

        i += 1  

我們來調用它看看輸出:

>>> for i in test(5):
print i


輸出:  
0
1
2
3
4
這場景是不是很類似 for i in xrange(len); 是的, xrange就是這麼幹的。 for .. in 的操作實際上是調用了生成器的next()方法,以上的調用過程可以等價爲:

[python]

f = test(5)  

print f.next()  

print f.next()  

print f.next()  

print f.next()  

print f.next()  

輸出結果與上次輸出一致。
另外,在這次調用過程中,協程被創建了一次, 被喚醒了5次(通過next),被掛起了5次(通過yield), 最後協程退出並銷燬。 大概就這些點了,有更深的理解再做補充。 

生成器(Generator)

如果列表元素可以按照某種算法推算出來,那我們是否可以在循環的過程中不斷推算出後續的元素呢?這樣就不必創建完整的list,從而節省大量的空間。在Python中,這種一邊循環一邊計算的機制,稱爲生成器(Generator)。


要創建一個generator,有很多種方法。第一種方法很簡單,只要把一個列表生成式的[]改成(),就創建了一個generator:


>>> L = [x * x for x in range(10)]

>>> L

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

>>> g = (x * x for x in range(10))

>>> g

<generator object <genexpr> at 0x104feab40>

創建L和g的區別僅在於最外層的[]和(),L是一個list,而g是一個generator。

我們可以直接打印出list的每一個元素,但我們怎麼打印出generator的每一個元素呢?

如果要一個一個打印出來,可以通過generator的next()方法:

>>> g.next()

0

>>> g.next()

1

>>> g.next()

4

>>> g.next()

9

>>> g.next()

16

>>> g.next()

25

>>> g.next()

36

>>> g.next()

49

>>> g.next()

64

>>> g.next()

81

>>> g.next()

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

StopIteration

我們講過,generator保存的是算法,每次調用next(),就計算出下一個元素的值,直到計算到最後一個元素,沒有更多的元素時,拋出StopIteration的錯誤。

當然,上面這種不斷調用next()方法實在是太變態了,正確的方法是使用for循環,因爲generator也是可迭代對象:

>>> g = (x * x for x in range(10))

>>> for n in g:

...     print n

...

0

1

4

9

16

25

36

49

64

81

所以,我們創建了一個generator後,基本上永遠不會調用next()方法,而是通過for循環來迭代它。

generator非常強大。如果推算的算法比較複雜,用類似列表生成式的for循環無法實現的時候,還可以用函數來實現。

比如,著名的斐波拉契數列(Fibonacci),除第一個和第二個數外,任意一個數都可由前兩個數相加得到:

1, 1, 2, 3, 5, 8, 13, 21, 34, ...

斐波拉契數列用列表生成式寫不出來,但是,用函數把它打印出來卻很容易:

def fib(max):

    n, a, b = 0, 0, 1

    while n < max:

        print b

        a, b = b, a + b

        n = n + 1

上面的函數可以輸出斐波那契數列的前N個數:

>>> fib(6)

1

1

2

3

5

8

仔細觀察,可以看出,fib函數實際上是定義了斐波拉契數列的推算規則,可以從第一個元素開始,推算出後續任意的元素,這種邏輯其實非常類似generator。

也就是說,上面的函數和generator僅一步之遙。要把fib函數變成generator,只需要把print b改爲yield b就可以了:

def fib(max):

    n, a, b = 0, 0, 1

    while n < max:

        yield b

        a, b = b, a + b

        n = n + 1

這就是定義generator的另一種方法。如果一個函數定義中包含yield關鍵字,那麼這個函數就不再是一個普通函數,而是一個generator:

>>> fib(6)

<generator object fib at 0x104feaaa0>

這裏,最難理解的就是generator和函數的執行流程不一樣。函數是順序執行,遇到return語句或者最後一行函數語句就返回。而變成generator的函數,在每次調用next()的時候執行,遇到yield語句返回,再次執行時從上次返回的yield語句處繼續執行。

舉個簡單的例子,定義一個generator,依次返回數字1,3,5:

>>> def odd():

...     print 'step 1'

...     yield 1

...     print 'step 2'

...     yield 3

...     print 'step 3'

...     yield 5

...

>>> o = odd()

>>> o.next()

step 1

1

>>> o.next()

step 2

3

>>> o.next()

step 3

5

>>> o.next()

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

StopIteration

可以看到,odd不是普通函數,而是generator,在執行過程中,遇到yield就中斷,下次又繼續執行。執行3次yield後,已經沒有yield可以執行了,所以,第4次調用next()就報錯。

回到fib的例子,我們在循環過程中不斷調用yield,就會不斷中斷。當然要給循環設置一個條件來退出循環,不然就會產生一個無限數列出來。

同樣的,把函數改成generator後,我們基本上從來不會用next()來調用它,而是直接使用for循環來迭代:

>>> for n in fib(6):

...     print n

...

1

1

2

3

5

8

小結

generator是非常強大的工具,在Python中,可以簡單地把列表生成式改成generator,也可以通過函數實現複雜邏輯的generator。

要理解generator的工作原理,它是在for循環的過程中不斷計算出下一個元素,並在適當的條件結束for循環。對於函數改成的generator來說,遇到return語句或者執行到函數體最後一行語句,就是結束generator的指令,for循環隨之結束。


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