2016-05-17 14 views
8

Z tego co rozumiem, o for x in a_generator: foo(x) pętli w Pythonie jest grubsza odpowiada to:Czy pętla przechodzi przez generator w pętli przez ten sam generator bezpieczny w Pythonie?

try: 
    while True: 
     foo(next(a_generator)) 
except StopIteration: 
    pass 

To sugeruje, że coś takiego:

for outer_item in a_generator: 
    if should_inner_loop(outer_item): 
     for inner_item in a_generator: 
      foo(inner_item) 
      if stop_inner_loop(inner_item): break 
    else: 
     bar(outer_item) 

byłoby zrobić dwie rzeczy:

  1. Nie wywołuj żadnych wyjątków, awarii ani niczego w tym rodzaju.
  2. Powtórz powyższe czynności, dopóki nie osiągnie jakiegoś x, gdzie should_inner_loop(x) wraca do prawdy, a następnie przełącza się na nią wewnętrznym for, aż do momentu, gdy stop_inner_loop(thing) zwróci wartość true. Następnie zewnętrzna pętla odzywa się , gdzie wewnętrzna pozostała wyłączona:.

Z moich, wprawdzie niezbyt dobrych testów, wynika, że ​​działa jak powyżej. Jednak nie znalazłem niczego w specyfikacji gwarantującej, że takie zachowanie jest stałe u tłumaczy. Czy jest coś, co mówi lub sugeruje, że mogę być pewien, że zawsze tak będzie? Czy może powodować błędy lub wykonywać je w inny sposób? (Czyli zrobić coś innego niż to, co opisano powyżej


NB Odpowiednik kodu powyżej pochodzi z własnego doświadczenia, nie wiem, czy to rzeczywiście dokładne Dlatego pytam

+3

Nawet jeśli twoja logika brzmi w odniesieniu do tego, które elementy są zużywane i kiedy, będzie to trudne dla kogoś innego (łącznie z twoim przyszłym "ja"), aby połączyć to, co się dzieje. –

+0

@JaredGoguen Zgadzam się. W moim prawdziwym kodzie, mam wewnętrzną pętlę wyodrębnioną do innej metody, ale myślałem, że intencja pytania byłaby jaśniejsza, gdybym nie miała innej metody. –

Odpowiedz

6

TL; DR: jest bezpieczna CPython (ale nie mogłem znaleźć żadnej specyfikacji tego), choć nie mogą robić to, co chcesz zrobić.


Po pierwsze, porozmawiajmy o twoim pierwszym założeniu, o równoważności.

Pętla for rzeczywiście wywołuje najpierw iter() na obiekcie, a następnie uruchamia next() na swoim wyniku, dopóki nie otrzyma StopIteration.

Oto odpowiedni kod bajtowy (forma niski poziom Python, używane przez samego tłumacza):

>>> import dis 
>>> def f(): 
... for x in y: 
... print(x) 
... 
>>> dis.dis(f) 
    2   0 SETUP_LOOP    24 (to 27) 
       3 LOAD_GLOBAL    0 (y) 
       6 GET_ITER 
     >> 7 FOR_ITER    16 (to 26) 
      10 STORE_FAST    0 (x) 

    3   13 LOAD_GLOBAL    1 (print) 
      16 LOAD_FAST    0 (x) 
      19 CALL_FUNCTION   1 (1 positional, 0 keyword pair) 
      22 POP_TOP 
      23 JUMP_ABSOLUTE   7 
     >> 26 POP_BLOCK 
     >> 27 LOAD_CONST    0 (None) 
      30 RETURN_VALUE 

GET_ITER połączeń iter(y) (która sama nazywa y.__iter__()) i pcha swój wynik na stosie (myśleć jako grupę lokalnych zmiennych bez nazwy), następnie wchodzi w pętlę pod numerem FOR_ITER, która wywołuje next(<iterator>) (która sama wywołuje <iterator>.__next__()), a następnie wykonuje kod wewnątrz pętli, a JUMP_ABSOLUTE powoduje, że wykonanie wraca do FOR_ITER.


Teraz dla bezpieczeństwa:

Oto metody generatora: https://hg.python.org/cpython/file/101404/Objects/genobject.c#l589 Jak widać na line 617, realizacja __iter__() jest PyObject_SelfIter, którego realizacja można znaleźć here. PyObject_SelfIter po prostu zwraca obiekt (tj. Sam generator).

Tak więc, po zagnieżdżeniu dwóch pętli, oba są iterowane w tym samym iteratorze. I, jak powiedziałeś, dzwonią po prostu next(), więc jest to bezpieczne.

Ale bądź ostrożny: wewnętrzna pętla pochłonie przedmioty, które nie będą zużyte przez zewnętrzną pętlę. Nawet jeśli to jest to, co chcesz zrobić, może to nie być bardzo czytelne.

Jeśli nie jest to, co chcesz zrobić, należy rozważyć itertools.tee(), który buforuje wyjście z iteratora, co pozwala na iteracje nad jego wyjściem dwa razy (lub więcej). Jest to skuteczne tylko wtedy, gdy trójniki są blisko siebie w strumieniu wyjściowym; Jeśli jeden iterator zostanie całkowicie wyczerpany, zanim drugi zostanie użyty, lepiej jest po prostu wywołać list w iteratorze, aby zmaterializować listę z niego.

+0

Hej, myślę, że ponieważ źle sformułowałem moje pytanie, otrzymałem odpowiedzi na niewłaściwe pytanie. Czy mógłbyś wyświadczyć mi przysługę i dwukrotnie sprawdzić tę odpowiedź za pomocą nieco zaktualizowanego pytania? –

+0

Punkt 1 był tym, co rozumiałem jako bezpieczeństwo, więc tak, to jest bezpieczne. Punkt 2 opisuje, co faktycznie robi (o czym mówiłem w dwóch ostatnich akapitach). –

+0

Awesome! Nie do końca zrozumiałam ostatnią kwestię dotyczącą 'itertools.tee()', ale wydawało się, że była to odpowiedź na błędne przekonanie, że opuściłem wszystkich. –

3

nr.. , to nie jest bezpieczne (jak w, nie otrzymamy wynik, że możemy się spodziewać)

Rozważ to:..

a = (_ for _ in range(20)) 
for num in a: 
    print(num) 

oczywiście, będziemy się od 0 do 19 drukowanego

Teraz dodajmy trochę kodu:

a = (_ for _ in range(20)) 
for num in a: 
    for another_num in a: 
     pass 
    print(num) 

Jedyną rzeczą, która zostanie wydrukowana jest 0. Zanim przejdziemy do drugiej iteracji zewnętrznej pętli, generator zostanie już wyczerpany przez wewnętrzną pętlę.

Możemy też to zrobić:

a = (_ for _ in range(20)) 
for num in a: 
    for another_num in a: 
     print(another_num) 

Gdyby to było bezpieczne spodziewamy się uzyskać od 0 do 19 wydrukowanych 20 razy, ale faktycznie się to drukowane tylko raz, z tego samego powodu, o którym wspomniałem wyżej.

+4

Cóż, zależy od oczekiwanego wyniku. Jeśli włączysz ideę 'break' używaną przez OP i jeśli zrozumiesz, że będzie ona kontynuowała iterację nad generatorem * od miejsca, w którym był *, to powiedziałbym, że otrzymasz oczekiwane zachowanie dla generatorów. – dwanderson

+0

Wyjaśniłem, co mam na myśli - to zachowanie jest tym, czego szukam. Dzięki za tonę! –

2

To nie jest odpowiedź na twoje pytanie, ale nie polecam tego, ponieważ kod jest nieczytelny. Zajęło mi trochę czasu, aby przekonać się, że dwukrotnie używasz y, mimo że jest to cały punkt Twojego pytania. Nie zmuszaj przyszłego czytelnika do tego. Kiedy widzę zagnieżdżoną pętlę, nie spodziewam się tego, co zrobiłeś i mój mózg ma problemy z jej zobaczeniem.

zrobiłbym to tak:

def generator_with_state(y): 
    state = 0 
    for x in y: 
     if isinstance(x, special_thing): 
      state = 1 
      continue 
     elif state == 1 and isinstance(x, signal): 
      state = 0 
     yield x, state 

for x, state in generator_with_state(y): 
    if state == 1: 
     foo(x) 
    else: 
     bar(x) 
+0

Tak właśnie robię w moim prawdziwym kodzie. Zapytałem z nieco bardziej zagmatwanym, ponieważ myślałem, że intencja będzie wyraźniejsza - pętla nad generatorem wewnątrz innej pętli. Przeprosiny. –

+0

Nie musisz przepraszać. Nie zawsze jest oczywiste, co spowoduje, że kod będzie mniej lub bardziej czytelny. Ale wydaje mi się, że fakt, że wszyscy wydają się mieć wątpliwości co do twojego pytania, pokazuje moją rację. –

+0

Odtworzyć kod i tak naprawdę ... nie jest to podobne do tego, co robię. Mogłem tylko rzucić okiem na telefon komórkowy, ale on też był podobny; mój aktualny kod jest taki, że definiuję funkcję, która wykonuje pętlę, a następnie, jeśli dzieje się coś szczególnego, wywołanie tej funkcji zamiast posiadania wewnętrznej pętli. Niestety ten schemat nie zadziałałby dla mnie; poza tym, wygląda dobrze! –