2010-04-10 1 views
11

Jakie są przesłanki leżące u podstaw zalecanego konstruowania pętli w stylu Python w stylu for i in xrange(...)? W przypadku prostych pętli całkowitych różnica w kosztach ogólnych jest znaczna. Przeprowadziłem prosty test za pomocą dwóch kawałków kodu:Uzasadnienie preferowanego języka Pythona dla składni

plików idiomatic.py:

#!/usr/bin/env python 

M = 10000 
N = 10000 

if __name__ == "__main__": 
    x, y = 0, 0 
    for x in xrange(N): 
     for y in xrange(M): 
      pass 

pliku cstyle.py:

#!/usr/bin/env python 

M = 10000 
N = 10000 

if __name__ == "__main__": 
    x, y = 0, 0 
    while x < N: 
     while y < M: 
      y += 1 
     x += 1 

wyniki profilowania były następujące:

bash-3.1$ time python cstyle.py 

real 0m0.109s 
user 0m0.015s 
sys  0m0.000s 

bash-3.1$ time python idiomatic.py 

real 0m4.492s 
user 0m0.000s 
sys  0m0.031s 

mogę zrozum, dlaczego wersja Pythonic jest wolniejsza - wyobrażam to sobie ma wiele wspólnego z wywoływaniem xrange N razy, być może można to wyeliminować, gdyby istniał sposób na zwinięcie generatora. Jednak przy tej różnicy w czasie wykonywania, dlaczego wolelibyśmy używać wersji Pythonic?

Edit: przeprowadziłem testy ponownie przy użyciu kodu Pan Martelli przewidziane, a wyniki były istotnie lepsze teraz:

pomyślałem, że wyliczyć wnioski z wątku tutaj:

1) Dużo kodu w zakresie modułu jest zły pomysł,, nawet jeśli kod jest zamknięty w bloku if __name__ == "__main__":.

2) * Co ciekawe, modyfikowania kodu, który należał do thebadone do mojego niewłaściwej wersji (y pozwalając rosnąć bez resetowania) produkowane małą różnicę w wydajności, nawet dla większych wartości M i N.

+2

Twój czas jest błędny, tak myślę. Przeprowadź wiele prób i być może wykonuj jakieś obliczenia, aby pozbyć się jakiejkolwiek możliwej optymalizacji z pętli. – Yuliy

+0

+1 Bardzo interesujące pytanie. Po przeczytaniu odpowiedzi Martinellego pytanie to jest dla mnie jeszcze bardziej interesujące, ponieważ pokazuje subtelne różnice między przywoływaniem fragmentu kodu wewnątrz i na zewnątrz funkcji. – OscarRyz

+1

-1: Ponieważ podstawą pytania był zasadniczo niepoprawny kod, proszę zamknąć pytanie. –

Odpowiedz

22

Oto właściwe porównanie, np. w loop.py:

M = 10000 
N = 10000 

def thegoodone(): 
    for x in xrange(N): 
     for y in xrange(M): 
      pass 

def thebadone(): 
    x = 0 
    while x < N: 
     y = 0 
     while y < M: 
      y += 1 
     x += 1 

Wszystko znaczny kod powinien zawsze być w funkcji - wprowadzenie sto milionów pętle na najwyższym poziomie modułu pokazuje lekkomyślne lekceważenie dla wydajności i sprawia, że ​​kpiny z wszelkimi próbami pomiaru wspomniana wydajność.

Gdy już to zrobisz, zobaczysz:

$ python -mtimeit -s'import loop' 'loop.thegoodone()' 
10 loops, best of 3: 3.45 sec per loop 
$ python -mtimeit -s'import loop' 'loop.thebadone()' 
10 loops, best of 3: 10.6 sec per loop 

więc odpowiednio zmierzyć, zły sposób, że adwokat jest około 3 razy wolniej niż dobry sposób, który promuje Python. Mam nadzieję, że spowoduje to ponowne rozważenie twojego błędnego poparcia.

+4

"Cały znaczący kod powinien zawsze znajdować się w funkcjach - umieszczenie stu milionów pętli na najwyższym poziomie modułu pokazuje lekkomyślne lekceważenie wydajności" (Rozumiem.) Czy mógłbyś wyjaśnić dlaczego? –

+6

@Federico, ** prędkość **. Zmienna liczba wejść i wyjść jest wysoce zoptymalizowana pod względem funkcji, ale nie może znajdować się na najwyższym poziomie modułu. Np. Zakładając, że mój laptop i maszyna Glenna są ekwiwalentne, z naszych liczb widzimy współczynnik 2 różnicy za robienie rzeczy we właściwy sposób (cały znaczący kod w funkcjach) w zupełnie niewłaściwy sposób (znaczący kod na najwyższym poziomie modułu). Zrób to sam! -) –

+1

Po prostu to zrobiłem. Na poziomie modułu: 14,3 s vs. 36,0 s. Lokalny do funkcjonowania: 8,6 s vs 18,5 s. (!!) Nie wiedziałem, dziękuję. –

11

zapomniałeś zresetuj y do 0 po wewnętrznej pętli.

#!/usr/bin/env python 
M = 10000 
N = 10000 

if __name__ == "__main__": 
    x, y = 0, 0 
    while x < N: 
     while y < M: 
      y += 1 
     x += 1 
     y = 0 

ed: 20.63s po fix vs. 6.97s wykorzystaniem XRange

+0

Chciałem odpowiedzieć podobnie. Mój skorygowany kod pętli while działał około 3 razy wolniej niż kod pętli for. –

+1

Z całą powagą, nie. Idiomy językowe zawsze muszą brać pod uwagę rozsądną wydajność; jeśli idiom xrange naprawdę * był * 40 razy wolniejszy, byłby to błędny idiom, który powinien być albo naprawiony, albo już nieużywany.Czytelność jest ważna, często nawet kosztem pewnych osiągnięć - ale nie aż tak dużo. –

+0

OK i tak usunęłam odpowiedź ... –

3

dobro dla iteracji nad strukturami danych

Składnia for i in ... jest dobre dla iteracji nad strukturami danych. W języku niższego poziomu, ogólnie byłoby iteracji przez tablicę indeksowane przez int, ale ze składnią Pythona można wyeliminować krok indeksowania.

0

Powtórzyłem test z @Alex Martelli's answer. Idiomatycznym do pętli 5 razy szybciej niż pętli:

python -mtimeit -s'from while_vs_for import while_loop as loop' 'loop(10000)' 
10 loops, best of 3: 9.6 sec per loop 
python -mtimeit -s'from while_vs_for import for_loop as loop' 'loop(10000)' 
10 loops, best of 3: 1.83 sec per loop 

while_vs_for.py:

def while_loop(N): 
    x = 0 
    while x < N: 
     y = 0 
     while y < N: 
      pass 
      y += 1 
     x += 1 

def for_loop(N): 
    for x in xrange(N): 
     for y in xrange(N): 
      pass 

Na poziomie modułu:

$ time -p python for.py 
real 4.38 
user 4.37 
sys 0.01 
$ time -p python while.py 
real 14.28 
user 14.28 
sys 0.01 
1

nie jest bezpośrednia odpowiedź na pytanie, ale chcę otworzyć okno dialogowe nieco bardziej na xrange(). dwie rzeczy:

A. coś jest nie tak z jednej z wypowiedzi PO, że nikt jeszcze nie skorygowanych (tak, oprócz błędu w kodzie nie resetowania y):

„Wyobrażam sobie, że ma wiele wspólnego z wywołaniem XRange N razy ....”

przeciwieństwie do tradycyjnego liczenia for pętle, Python jest bardziej jak powłoka na foreach ... zapętlenie nad iterable. dlatego xrange() nazywa się dokładnie raz, a nie "N razy".

B. xrange() to nazwa tej funkcji w Pythonie 2. Zastępuje ona i zmienia nazwę na range() w Pythonie 3, więc należy o tym pamiętać podczas przenoszenia. jeśli jeszcze nie wiesz, xrange() zwraca iterator (podobnie do obiektu), podczas gdy range() zwraca listę. ponieważ ten drugi jest bardziej nieefektywny, został przestarzały na rzecz xrange(), który jest bardziej przyjazny dla pamięci. obejście w Pythonie 3, dla wszystkich, którzy potrzebują , aby mieć listę: list(range(N)).

+0

Pomyślałem, że za każdym razem, gdy wewnętrzna pętla zakończyła wszystkie iteracje, obiekt xrange (M) został ponownie zdefiniowany, ponieważ AFAIK, generatory nie mogły być przewinięte. – susmits