2012-05-04 8 views
6

Podczas odpowiadania na pytanie Clunky calculation of differences between an incrementing set of numbers, is there a more beautiful way?, znalazłem dwa rozwiązania, jeden z List Comprehension i inne przy użyciu itertools.starmap.Kiedy `starmap` może być preferowany zamiast `List Comprehension`

Dla mnie, list comprehension Składnia wygląda na bardziej przejrzystą, czytelną, mniej gadatliwą i bardziej Pythoniczną. Ale wciąż jako starmap jest dobrze dostępny w itertools, zastanawiałem się, musi być powód dla niego.

Moje pytanie brzmi, kiedy starmap może być preferowany nad List Comprehension?

Uwaga Jeśli jego sprawą Style to zdecydowanie zaprzecza There should be one-- and preferably only one --obvious way to do it.

Head to Head Porównanie

Czytelność liczy. --- LC

Its znowu kwestia percepcji, ale mi LC jest bardziej czytelny niż starmap. Aby użyć starmap, należy zaimportować operator lub zdefiniować lambda lub pewną jawną funkcję multi-variable, a mimo to dodatkowy import z itertools.

Wydajność --- LC

>>> def using_star_map(nums): 
    delta=starmap(sub,izip(nums[1:],nums)) 
    return sum(delta)/float(len(nums)-1) 
>>> def using_LC(nums): 
    delta=(x-y for x,y in izip(nums[1:],nums)) 
    return sum(delta)/float(len(nums)-1) 
>>> nums=[random.randint(1,10) for _ in range(100000)] 
>>> t1=Timer(stmt='using_star_map(nums)',setup='from __main__ import nums,using_star_map;from itertools import starmap,izip') 
>>> t2=Timer(stmt='using_LC(nums)',setup='from __main__ import nums,using_LC;from itertools import izip') 
>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=1000)/100000) 
235.03 usec/pass 
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=1000)/100000) 
181.87 usec/pass 
+0

Nie sądzę, że to sprawiedliwe, aby porównać je tak, jak ty. Obie funkcje powinny zapisać różnice do 'deltas', ponieważ w chwili obecnej' using_star_map' jest mniej czytelny, ponieważ wszystko znajduje się w jednym wierszu. Zmień go na: 'deltas = starmap (sub, zip (num [1:], nums))' 'sum (deltas)/float (len (nums) -1)' – jamylak

+0

@jamylak: Dziękuję za wskazanie tego. Ale niestety nie zmienia to różnicy w wydajności. – Abhijit

+0

Nie miało to na celu, ale mówimy również o czytelności. – jamylak

Odpowiedz

3

Jest to w dużej mierze sprawa styl. Wybierz tę, która jest bardziej czytelna.

w stosunku do „Jest tylko jeden sposób, aby to zrobić”, Sven Marnach uprzejmie zapewnia to Guido quote:

„Możesz myśleć, to jest niezgodny TOOWTDI, ale jak już mówiłem wcześniej, że było białe kłamstwo (jak również bezczelna reakcja na slogan Perla pod numerem 2000). Będąc w stanie wyrazić intencję (dla czytelników człowieka) często wymaga wyboru między wieloma formami, które wykonują w zasadzie to samo, jednak wyglądać inaczej dla czytelnika.”

W hotspot wydajności, warto wybrać rozwiązanie, które działa najszybciej (co jak sądzę w tym przypadku byłoby oparte na starmap).

Na wyniki - starmap jest wolniejszy z powodu destrukturyzacji; jednak nie jest to konieczne Starmap tutaj:

from timeit import Timer 
import random 
from itertools import starmap, izip,imap 
from operator import sub 

def using_imap(nums): 
    delta=imap(sub,nums[1:],nums[:-1]) 
    return sum(delta)/float(len(nums)-1) 

def using_LC(nums): 
    delta=(x-y for x,y in izip(nums[1:],nums)) 
    return sum(delta)/float(len(nums)-1) 

nums=[random.randint(1,10) for _ in range(100000)] 
t1=Timer(stmt='using_imap(nums)',setup='from __main__ import nums,using_imap') 
t2=Timer(stmt='using_LC(nums)',setup='from __main__ import nums,using_LC') 

Na moim komputerze:

>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=1000)/100000) 
172.86 usec/pass 
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=1000)/100000) 
178.62 usec/pass 

imap wychodzi odrobinę szybciej, prawdopodobnie dlatego, że unika skompresowanie/rozpad.

+0

Ale Zen z Python mówi: "Powinien istnieć jeden - a najlepiej tylko jeden - czysty sposób na zrobienie tego." – Abhijit

+0

@Ahhijit Tak, ale nie powinieneś wierzyć we wszystko, co czytasz. Python często ma kilka równie dobrych sposobów na wykonanie danego zadania. – Marcin

+0

Ale czy nie jest to podstawowa filozofia Pythona, która odróżnia ją od 'Ruby',' Perl' ... – Abhijit

10

Różnica, którą normalnie widzę, to: map()/starmap() są najbardziej odpowiednie, gdy dosłownie po prostu wywołasz funkcję na każdym elemencie na liście.W tym przypadku, są one trochę jaśniej:

(f(x) for x in y) 
map(f, y) # itertools.imap(f, y) in 2.x 

(f(*x) for x in y) 
starmap(f, y) 

Jak najszybciej rozpocząć potrzeby rzucać w lambda lub filter jak dobrze, należy przełączyć się do wypowiedzi lista comp/prądnicy, ale w przypadkach, gdy jest to pojedyncza funkcja, składnia wydaje się bardzo gadatliwa dla wyrażenia generowania listowego ze zrozumieniem.

one wymienne, a gdzie masz wątpliwości, przyklejają się do wypowiedzi generatora, ponieważ jest bardziej czytelny w ogóle, ale w prostym przypadku (map(int, strings), starmap(Vector, points)) używając map()/starmap() czasami może dokonać rzeczy łatwiejsze do odczytania.

Przykład:

Przykładem gdzie myślę starmap() jest bardziej czytelny:

from collections import namedtuple 
from itertools import starmap 

points = [(10, 20), (20, 10), (0, 0), (20, 20)] 

Vector = namedtuple("Vector", ["x", "y"]) 

for vector in (Vector(*point) for point in points): 
    ... 

for vector in starmap(Vector, points): 
    ... 

I map():

values = ["10", "20", "0"] 

for number in (int(x) for x in values): 
    ... 

for number in map(int, values): 
    ... 

Wydajność:

python -m timeit -s "from itertools import starmap" -s "from operator import sub" -s "numbers = zip(range(100000), range(100000))" "sum(starmap(sub, numbers))"       
1000000 loops, best of 3: 0.258 usec per loop 

python -m timeit -s "numbers = zip(range(100000), range(100000))" "sum(x-y for x, y in numbers)"       
1000000 loops, best of 3: 0.446 usec per loop 

do budowy namedtuple:

python -m timeit -s "from itertools import starmap" -s "from collections import namedtuple" -s "numbers = zip(range(100000), reversed(range(100000)))" -s "Vector = namedtuple('Vector', ['x', 'y'])" "list(starmap(Vector, numbers))" 
1000000 loops, best of 3: 0.98 usec per loop 

python -m timeit -s "from collections import namedtuple" -s "numbers = zip(range(100000), reversed(range(100000)))" -s "Vector = namedtuple('Vector', ['x', 'y'])" "[Vector(*pos) for pos in numbers]" 
1000000 loops, best of 3: 0.375 usec per loop 

W moich testach, gdzie mówimy o użyciu prostych funkcji (nr lambda), starmap() jest szybszy niż odpowiednik wyrazu generatora. Oczywiście, wydajność powinna zająć tylne miejsce, aby zapewnić czytelność, chyba że jest to sprawdzone wąskie gardło.

Przykład jak lambda zabija wszelkie zyski wydajności, sam przykład jak w pierwszym secie, ale lambda zamiast operator.sub():

python -m timeit -s "from itertools import starmap" -s "numbers = zip(range(100000), range(100000))" "sum(starmap(lambda x, y: x-y, numbers))" 
1000000 loops, best of 3: 0.546 usec per loop 
+0

'map (f, y)' jest równoważne dla '[f (x) dla x in y]', a nie dla '(f (x) dla x in y)', ponieważ nie jest to generator. Wykonuje się od razu. – akaRem

+1

@akaRem Lattyware zawsze używa python 3. – Marcin

+0

@akaRem Przepraszam, mówię Python 3.x - w rzeczy samej, w 2.x, to prawda. Zaktualizowano, aby wyjaśnić. –

0

O Starmap .. Powiedzmy, że masz L = [(0,1,2),(3,4,5),(6,7,8),..].

Generator comprehansion wyglądałby

(f(a,b,c) for a,b,c in L) 

lub

(f(*item) for item in L) 

I Starmap wyglądałby

starmap(f, L) 

Trzeci wariant jest lżejsza i krótsza. Ale pierwsza jest bardzo oczywista i nie zmusza mnie do tego, co robi.

Ok. Teraz chcę napisać bardziej skomplikowane w jednej linii kodu ..

some_result = starmap(f_res, [starmap(f1,L1), starmap(f2,L2), starmap(f3,L3)]) 

Linia ta nie jest oczywista, ale nadal łatwe do zrozumienia .. W generatorze comprehansion to będzie wyglądać:

some_result = (f_res(a,b,c) for a,b,c in [(f1(a,b,c) for a,b,c in L1), (f2(a,b,c) for a,b,c in L2), (f3(a,b,c) for a,b,c in L3)]) 

Jak widać, jest to długa, ciężka do zrozumienia i nie mogą być umieszczone w jednej linii, ponieważ jest większy niż 79 znaków (PEP 8). Nawet krótszy wariant jest zły:

some_result = (f_res(*item) for item [(f1(*item) for item in L1), (f(*item2) for item in L2), (f3(*item) for item in L3)]) 

Zbyt wiele znaków .. Za dużo nawiasów .. Za dużo hałasu.

So. Starmap w niektórych przypadkach jest bardzo przydatnym narzędziem. Dzięki niemu możesz napisać mniej kodu, który jest prostszy do zrozumienia.

EDIT dodano kilka obojętne testy

from timeit import timeit 
print timeit("from itertools import starmap\nL = [(0,1,2),(3,4,5),(6,7,8)]\nt=list((max(a,b,c)for a,b,c in L))") 
print timeit("from itertools import starmap\nL = [(0,1,2),(3,4,5),(6,7,8)]\nt=list((max(*item)for item in L))") 
print timeit("from itertools import starmap\nL = [(0,1,2),(3,4,5),(6,7,8)]\nt=list(starmap(max,L))") 

wyjścia (python 2.7.2)

5.23479851154 
5.35265309689 
4.48601346328 

Więc Starmap nawet ~ 15% szybciej tutaj.

+0

Twierdzę, że w bardziej złożonym przypadku oba wyrażenia "starmap()" i generatora nie są dobrym rozwiązaniem. Bardziej czytelny jest rozwinięcie twojego kodu (na przykład pełnego generatora) w tym punkcie. –

+0

Czy "za dużo szumu" jest błędem gramatycznym, czy jest to celowe? – jamylak

+0

Angielski nie jest moim ojczystym językiem. Mam na myśli wiele różnych znaków, które powodują hałas wizualny. I dlatego ten kod jest trudny nawet do prostego czytania (bez próby zrozumienia). – akaRem

Powiązane problemy