Python 3.x: Тествайте дали генераторът има оставащи елементи

Когато използвам генератор в for цикъл, изглежда, че "знае", когато няма повече дадени елементи. Сега трябва да използвам генератор БЕЗ for цикъл и да използвам next() на ръка, за да получа следващия елемент. Проблемът ми е, как да разбера, ако няма повече елементи?

Знам само: next() повдига изключение (StopIteration), ако не е останало нищо, НО не е ли изключение малко "тежко" за толкова прост проблем? Няма ли метод като 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

Благодаря на всички и хубав ден! :)


person madamada    schedule 22.12.2011    source източник


Отговори (4)


Двете твърдения, които сте написали, се занимават с намирането на края на генератора по абсолютно същия начин. For-цикълът просто извиква .next(), докато не бъде повдигнато изключението StopIteration и след това прекратява.

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

Като такъв не мисля, че изчакването на изключението StopIteration е „тежък“ начин за справяне с проблема, това е начинът, по който генераторите са проектирани да бъдат използвани.

person MDT    schedule 22.12.2011

Това е страхотен въпрос. Ще се опитам да ви покажа как можем да използваме интроспективните способности на 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, така че нека потърсим FOR_ITER в изходния код на интерпретатора на CPython. Ако се поровите наоколо, ще го намерите в Python/ceval.c; можете да го видите тук. Ето всичко:

    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))
person Ori    schedule 28.12.2011
comment
Току-що разбрах, че искам да направя dis за изучаване на numpy. ;) - person n611x007; 05.11.2013

Не е възможно да се знае предварително за края на итератора в общия случай, тъй като може да се наложи да се изпълни произволен код, за да се вземе решение за края. Буфериращите елементи биха могли да помогнат за разкриването на неща на цена - но това рядко е полезно.

На практика въпросът възниква, когато някой иска да вземе само един или няколко елемента от итератор за сега, но не иска да напише този грозен код за обработка на изключения (както е посочено във въпроса). Всъщност не е питонично да се постави концепцията "StopIteration" в нормален код на приложение. И обработката на изключения на ниво Python отнема доста време - особено когато става въпрос само за вземане на един елемент.

Питоничният начин за справяне с тези ситуации най-добре е или използването на for .. break [.. else] като:

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

или с помощта на вградената функция next() със стандартен like

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__() не трябва да се използва от нормален код на приложение. Ето защо го преименуваха от iterator.next() в Python2. И използването на next() без подразбиране не е много по-добро ...

person kxr    schedule 23.02.2016

Това може да не отговаря точно на въпроса ви, но намерих пътя си тук, търсейки елегантно да взема резултат от генератор, без да се налага да пиша try: блок. Малко гугъл по-късно разбрах това:

def g():
    yield 5

result = next(g(), None)

Сега result е или 5, или None, в зависимост от това колко пъти сте извикали next на итератора, или в зависимост от това дали функцията генератор се е върнала по-рано, вместо да даде резултат.

Силно предпочитам да боравя с None като изход пред рейз за "нормални" условия, така че избягването на опита/хващането тук е голяма победа. Ако ситуацията го изисква, има и лесно място за добавяне на стойност по подразбиране, различна от None.

person Chris    schedule 15.05.2018