2011-12-22 43 views
9

當我在for循環中使用生成器時,它似乎「知道」,當沒有更多的元素產生時。現在,我必須使用一個沒有for循環的發生器,然後使用下一個()來獲得下一個元素。我的問題是,我怎麼知道,如果沒有更多的元素?Python 3.x:測試發生器是否有元素剩餘

我只知道:下一個()會引發一個異常(StopIteration),如果沒有剩下任何東西,但對於這樣一個簡單的問題,BUT不是一個例外太「重」嗎?是不是有像has_next()左右的方法?

以下行應該清楚,我的意思:

#!/usr/bin/python3 

# define a list of some objects 
bar = ['abc', 123, None, True, 456.789] 

# our primitive generator 
def foo(bar): 
    for b in bar: 
     yield b 

# iterate, using the generator above 
print('--- TEST A (for loop) ---') 
for baz in foo(bar): 
    print(baz) 
print() 

# assign a new iterator to a variable 
foobar = foo(bar) 

print('--- TEST B (try-except) ---') 
while True: 
    try: 
     print(foobar.__next__()) 
    except StopIteration: 
     break 
print() 

# assign a new iterator to a variable 
foobar = foo(bar) 

# display generator members 
print('--- GENERATOR MEMBERS ---') 
print(', '.join(dir(foobar))) 

輸出如下:

--- TEST A (for loop) --- 
abc 
123 
None 
True 
456.789 

--- TEST B (try-except) --- 
abc 
123 
None 
True 
456.789 

--- GENERATOR MEMBERS --- 
__class__, __delattr__, __doc__, __eq__, __format__, __ge__, __getattribute__, __gt__, __hash__, __init__, __iter__, __le__, __lt__, __name__, __ne__, __new__, __next__, __reduce__, __reduce_ex__, __repr__, __setattr__, __sizeof__, __str__, __subclasshook__, close, gi_code, gi_frame, gi_running, send, throw 

感謝大家,並有一個愉快的一天! :)

回答

5

你寫的兩個陳述處理以完全相同的方式找到生成器的結尾。 for循環直接調用.next(),直到引發StopIteration異常,然後終止。

http://docs.python.org/tutorial/classes.html#iterators

因此我不認爲等待StopIteration異常是一個「重」的方式來處理這個問題,它的發電機在設計中使用的方式。

16

這是一個很好的問題。我將嘗試向您展示如何使用Python的內省功能和開源獲得答案。我們可以使用dis模塊來窺視幕後,看看CPython解釋器如何在迭代器上實現for循環。

>>> def for_loop(iterable): 
...  for item in iterable: 
...   pass # do nothing 
...  
>>> import dis 
>>> dis.dis(for_loop) 
    2   0 SETUP_LOOP    14 (to 17) 
       3 LOAD_FAST    0 (iterable) 
       6 GET_ITER    
     >> 7 FOR_ITER     6 (to 16) 
      10 STORE_FAST    1 (item) 

    3   13 JUMP_ABSOLUTE   7 
     >> 16 POP_BLOCK    
     >> 17 LOAD_CONST    0 (None) 
      20 RETURN_VALUE   

多汁的位似乎是FOR_ITER操作碼。我們不能深入使用dis,所以讓我們在CPython解釋器的源代碼中查找FOR_ITER。如果你四處遊蕩,你會發現它在Python/ceval.c;你可以查看它here。這是整個事情:

TARGET(FOR_ITER) 
     /* before: [iter]; after: [iter, iter()] *or* [] */ 
     v = TOP(); 
     x = (*v->ob_type->tp_iternext)(v); 
     if (x != NULL) { 
      PUSH(x); 
      PREDICT(STORE_FAST); 
      PREDICT(UNPACK_SEQUENCE); 
      DISPATCH(); 
     } 
     if (PyErr_Occurred()) { 
      if (!PyErr_ExceptionMatches(
          PyExc_StopIteration)) 
       break; 
      PyErr_Clear(); 
     } 
     /* iterator ended normally */ 
     x = v = POP(); 
     Py_DECREF(v); 
     JUMPBY(oparg); 
     DISPATCH(); 

你看到這是如何工作的?我們嘗試從迭代器中獲取一個項目;如果我們失敗了,我們檢查引發了什麼異常。如果它是StopIteration,我們將其清除並考慮迭代器已耗盡。

那麼,當迭代器已經耗盡時,for循環如何「知道」?答:它不 - 它必須嘗試並抓住一個元素。但爲什麼?

部分答案很簡單。部分實現迭代器的好處是你只需要定義一個操作:獲取下一個元素。但更重要的是,它使得迭代器懶惰:它們只會產生他們絕對必須的值。

最後,如果你真的錯過了這個功能,你自己實現它是微不足道的。這裏有一個例子:

class LookaheadIterator: 

    def __init__(self, iterable): 
     self.iterator = iter(iterable) 
     self.buffer = [] 

    def __iter__(self): 
     return self 

    def __next__(self): 
     if self.buffer: 
      return self.buffer.pop() 
     else: 
      return next(self.iterator) 

    def has_next(self): 
     if self.buffer: 
      return True 

     try: 
      self.buffer = [next(self.iterator)] 
     except StopIteration: 
      return False 
     else: 
      return True 


x = LookaheadIterator(range(2)) 

print(x.has_next()) 
print(next(x)) 
print(x.has_next()) 
print(next(x)) 
print(x.has_next()) 
print(next(x)) 
+1

我剛剛意識到我想爲學習'numpy'做'dis'。 ;) – n611x007 2013-11-05 11:32:13

0

這是不可能事先約結束迭代器在一般情況下知道了,因爲任意代碼可能需要運行,以決定有關結束。緩衝元素可以幫助揭示成本 - 但這很少有用。

實際上,如果現在想從迭代器中只取一個或幾個元素,但不想寫出醜陋的異常處理代碼(如問題中所示),則會出現問題。事實上,將概念「StopIteration」放入正常的應用程序代碼中是非pythonic。而python級別的異常處理相當耗時 - 特別是當它只涉及一個元素時。

的Python的方式使用for .. break [.. else]等來處理這些情況最好可以是:

for x in iterator: 
    do_something(x) 
    break 
else: 
    it_was_exhausted() 

或使用內置next()功能與默認像

x = next(iterator, default_value) 

,或者使用迭代器助手例如從itertools模塊像再佈線的東西:

max_3_elements = list(itertools.islice(iterator, 3)) 

一些迭代然而暴露 「長度暗示」(PEP424):

>>> gen = iter(range(3)) 
>>> gen.__length_hint__() 
3 
>>> next(gen) 
0 
>>> gen.__length_hint__() 
2 

注:iterator.__next__()不應由正常的應用代碼中使用。這就是爲什麼他們在Python2中將其重命名爲iterator.next()。而使用next()沒有默認情況下好不了多少...