2013-05-09 10 views
14

Dlaczego w funkcji np kończy:Jak dochodzi do zatrzymania wyjątku StopIteration?

def func(iterable): 
    while True: 
     val = next(iterable) 
     yield val 

ale gdybym startu funkcja oświadczenie wydajność podniesie wyjątek StopIteration?

EDIT: Przepraszamy za to, że wprowadziliśmy w błąd. Wiem, czym są generatory i jak z nich korzystać. Oczywiście kiedy powiedziałem, że funkcja się kończy, nie miałem na myśli gorliwej oceny funkcji. Właśnie do zrozumienia, że ​​podczas korzystania z funkcji produkować generator:

gen = func(iterable) 

w przypadku func to działa i zwraca ten sam generator, ale w przypadku func2:

def func2(iterable): 
    while True: 
     val = next(iterable) 

podnosi StopIteration zamiast Brak powrót lub nieskończona pętla.

Pozwól, że będę dokładniejszy. Istnieje funkcja tee w itertools co jest równoważne:

def tee(iterable, n=2): 
    it = iter(iterable) 
    deques = [collections.deque() for i in range(n)] 
    def gen(mydeque): 
     while True: 
      if not mydeque:    # when the local deque is empty 
       newval = next(it)  # fetch a new value and 
       for d in deques:  # load it to all the deques 
        d.append(newval) 
      yield mydeque.popleft() 
    return tuple(gen(d) for d in deques) 

istnieje w rzeczywistości, niektóre magiczne, ponieważ funkcja zagnieżdżona gen ma nieskończoną pętlę bez sprawozdania złamać. Funkcja gen kończy się z powodu wyjątku StopIteration, gdy nie ma elementów w it. Ale kończy się poprawnie (bez podnoszenia wyjątków), tj. Po prostu zatrzymuje pętlę. Pytanie brzmi:: gdzie jest Zatrzymanie jest obsługiwane?

+0

Jak się nazywasz? –

Odpowiedz

24

Aby odpowiedzieć na pytanie, gdzie StopIteration wpada w generator utworzony wewnątrz itertools.tee: nie. Odbiorca musi odnieść się do wyników tee, aby wychwycić wyjątek podczas ich iteracji.

Po pierwsze, należy zauważyć, że funkcja generatora (która jest dowolną funkcją z instrukcją yield w dowolnym miejscu) różni się zasadniczo od normalnej funkcji. Zamiast uruchamiać kod funkcji po wywołaniu, otrzymasz po prostu obiekt generator podczas wywoływania funkcji. Tylko wtedy, gdy wykonasz iterację nad generatorem, uruchomisz kod.

Funkcja generatora nigdy nie zakończy iterowania bez podniesienia StopIteration (chyba że podniesie to inny wyjątek). StopIteration to sygnał z generatora, że ​​jest on wykonywany i nie jest opcjonalny. Jeśli osiągniesz instrukcję return lub koniec kodu funkcji generatora bez podnoszenia czegokolwiek, Python podniesie dla ciebie StopIteration!

To różni się od zwykłych funkcji, które zwracają None, jeśli osiągną koniec bez zwracania czegokolwiek innego. Łączy się z różnymi sposobami działania generatorów, jak opisałem powyżej.

Oto przykład funkcja generator, który sprawi, że łatwo zobaczyć, jak StopIteration zostanie podniesiony:

def simple_generator(): 
    yield "foo" 
    yield "bar" 
    # StopIteration will be raised here automatically 

Oto co się dzieje, kiedy go spożywać:

>>> g = simple_generator() 
>>> next(g) 
'foo' 
>>> next(g) 
'bar' 
>>> next(g) 
Traceback (most recent call last): 
    File "<pyshell#6>", line 1, in <module> 
    next(g) 
StopIteration 

Wywołanie simple_generator zawsze zwraca generator obiektu natychmiast (bez uruchamiania żadnego kodu w funkcji). Każde wywołanie next w obiekcie generatora uruchamia kod do następnej instrukcji yield i zwraca uzyskaną wartość. Jeśli nie ma więcej, aby uzyskać, podnosi się StopIteration.

Obecnie zwykle nie są wyświetlane wyjątki od StopIteration. Powodem tego jest fakt, że zwykle zużywasz generatory wewnątrz pętli for. Oświadczenie for automatycznie wywoła next w kółko, aż zostanie podniesione StopIteration. To złapie i zlikwiduje dla ciebie wyjątek StopIteration, więc nie musisz zmywać z blokami try/except, aby sobie z tym poradzić.

for pętla jak for item in iterable: do_suff(item) jest prawie dokładnie równoważne z niniejszym while pętli (jedyna różnica polega na tym, że prawdziwa for nie potrzebuje zmienną tymczasową trzymać iterator):

iterator = iter(iterable) 
try: 
    while True: 
     item = next(iterator) 
     do_stuff(item) 
except StopIteration: 
    pass 
finally: 
    del iterator 

Funkcja gen generator pokazany u góry jest jednym wyjątkiem. Używa on wyjątku StopIteration wytwarzanego przez iterator, który zużywa jako swój własny sygnał, że jest wykonywany na iteracji. Oznacza to, że zamiast wychwytywania StopIteration, a następnie zerwania z pętli, po prostu pozwala, aby wyjątek nie był przechwytywany (prawdopodobnie zostanie przechwycony przez jakiś kod wyższego poziomu).

Niezwiązane z głównym pytaniem, jest jeszcze jedna rzecz, którą chcę wskazać. W swoim kodzie wywołujesz next dla zmiennej o nazwie iterable. Jeśli weźmiesz to nazwisko jako dokumentację, jaki typ obiektu otrzymasz, nie jest to konieczne.

next jest częścią protokołu iterator, a nie iterable (lub container). Może działać dla niektórych rodzajów iteracji (takich jak pliki i generatory, ponieważ te typy są ich własnymi iteratorami), ale nie powiedzie się dla innych iteracji, takich jak krotki i listy. Im bardziej poprawne podejście, jest wywołanie iter na wartości iterable, a następnie wywołanie next w otrzymanym iteratorze. (Lub po prostu użyć for pętle, które wymagają zarówno iter i next dla Ciebie w odpowiednim czasie!)

Edit: Właśnie znalazłem moje własne odpowiedzi w wyszukiwarce Google dla powiązanego pytanie, a ja myślałem, że aktualizacji do punktu że powyższa odpowiedź nie będzie całkowicie prawdziwa w przyszłych wersjach Pythona. PEP 479 powoduje błąd polegający na tym, że StopIteration zapala się nieprzytomnie z funkcji generatora. Jeśli tak się stanie, Python zmieni go w wyjątek RuntimeError.

Oznacza to, że kod taki jak przykłady w itertools, które używają StopIteration do wyrwania się z funkcji generatora, będzie wymagał modyfikacji. Zazwyczaj będziesz musiał złapać wyjątek z try/except, a następnie return.

Ponieważ jest to niezgodna wstecz zmiana, jest ona stopniowo wprowadzana. W Pythonie 3.5, cały kod będzie działał jak poprzednio, ale możesz uzyskać nowe zachowanie za pomocą from __future__ import generator_stop. W Pythonie 3.6 kod nadal będzie działał, ale wyświetli ostrzeżenie. W Pythonie 3.7 nowe zachowanie będzie obowiązywać przez cały czas.

+0

Tak, StopIteracja jest zużywana przez definicję funkcji (lub równoważną strukturę generatora)? Chcę tylko dowiedzieć się, czy użyjemy następnego poza ciałem funkcji, które spowoduje wyjątek, ale jeśli użyjemy funkcji wewnętrznej, zakończy się ona normalnie. –

+1

@BranAlgue Nie, definicja funkcji nie spowoduje użycia wyjątku. Podobnie jak każdy inny wyjątek, 'StopIteration' przejdzie do stosu wywołań, dopóki nie zostanie przechwycony przez jawny blok' try'/'catch' lub przez niejawny w pętli' for'. Myślę, że brakuje Ci rzeczy, że 'StopIteracja' nie stanowi problemu w funkcji generatora. Oczekuje się, że go podniesiesz, kiedy nie będziesz miał nic do zaoferowania. Możesz to zrobić jawnie za pomocą 'raise StopIteration()' lub niejawnie, przechodząc do końca funkcji - lub możesz pozwolić, aby 'StopIteration' utworzony przez wywołanie' next' przestał działać. – Blckknght

+0

Rozumiem to. Nie rozumiem, dlaczego "StopIteracja" nie stanowi problemu w funkcji generatora. Czy twierdzenie, że generator niejawnie obsługuje wyjątek, jest poprawne? –

2

Bez numeru yield można powtórzyć cały proces iterable, nie przerywając pracy z val. Pętla while nie przechwytuje wyjątku StopIteration. Równoważne for pętla może być:

def func(iterable): 
    for val in iterable: 
     pass 

który ma złapać StopIteration i po prostu wyjść z pętli, a tym samym powrócić z funkcji.

Można jawnie złapać wyjątek:

def func(iterable): 
    while True: 
     try: 
      val = next(iterable) 
     except StopIteration: 
      break 
5

Gdy funkcja zawiera yield, nazywając to faktycznie nie wykonać czegokolwiek, po prostu tworzy obiekt generatora. Tylko iteracja nad tym obiektem spowoduje wykonanie kodu. Zgaduję więc, że po prostu wywołujesz funkcję, co oznacza, że ​​funkcja nie podnosi StopIteration, ponieważ nigdy nie jest wykonywana.

Biorąc swoją funkcję, oraz iterable:

def func(iterable): 
    while True: 
     val = next(iterable) 
     yield val 

iterable = iter([1, 2, 3]) 

To jest zły sposób to nazwać:

func(iterable) 

To jest właściwa droga:

for item in func(iterable): 
    # do something with item 

Ty może również przechowywać generator w zmiennej i wywoływać na nim next() (lub powtarzać to w jakiś inny sposób):

gen = func(iterable) 
print(next(gen)) # prints 1 
print(next(gen)) # prints 2 
print(next(gen)) # prints 3 
print(next(gen)) # StopIteration 

Nawiasem mówiąc, lepszy sposób napisać funkcję następująco:

def func(iterable): 
    for item in iterable: 
     yield item 

albo w Pythonie 3.3 i nowszym:

def func(iterable): 
    yield from iter(iterable) 

Oczywiście prawdziwe generatory rzadko są tak trywialne. :-)

+0

Ostrożnie z tym ostatnim przykładem. 'list' nie ma metody' next' lub '__next__', Twój przykład nie zadziała. Poprawka jest jednak łatwa. 'gen = func (iter ([1,2,3]))' – mgilson

+0

Tak, po prostu sobie sprawę, że, naprawione. :-) – kindall

0

yield nie łapie StopIteration. To, co robi yield dla twojej funkcji, powoduje, że staje się ona funkcją generującą, a nie zwykłą funkcją. Tak więc obiekt zwracany z wywołania funkcji jest obiektem iteracyjnym (który oblicza następną wartość, gdy poprosi się o nią funkcją next (która jest wywoływana niejawnie przez pętlę for)). Jeśli opuścisz instrukcję yield z tego, to python natychmiast wykona pętlę while, która kończy się wyczerpaniem iteracji (jeśli jest skończona) i podniesieniem StopIteration w prawo, gdy ją wywołasz.

rozważenia:

x = func(x for x in []) 
next(x) #raises StopIteration 

for pętla łapie wyjątek - Tak to się wie, kiedy przestać nazywać next na iterable Ci go dał.

Powiązane problemy