2012-05-04 3 views
6

Während der Beantwortung der Frage Clunky calculation of differences between an incrementing set of numbers, is there a more beautiful way?, kam ich mit zwei Lösungen, eine mit List Comprehension und andere mit itertools.starmap.Wenn `sternmap` über` List Comprehension` bevorzugt werden konnte

Für mich, list comprehension Syntax sieht luzider, lesbarer, weniger wortreich und mehr Pythonic. Aber immer noch als starmap ist gut verfügbar in iertools, ich frage mich, es muss einen Grund dafür geben.

Meine Frage ist, wenn starmap gegenüber List Comprehension bevorzugt werden könnte?

Hinweis Wenn ihr eine Frage des Stils dann auf jeden Fall es There should be one-- and preferably only one --obvious way to do it.

Head to Head

Ablesbarkeit zählt Vergleich widerspricht. --- LC

Sein wieder eine Frage der Wahrnehmung, aber für mich LC ist besser lesbar als starmap. Um starmap zu verwenden, müssen Sie entweder operator importieren oder lambda oder einige explizite multi-variable Funktion definieren und trotzdem einen Import von itertools.

Leistung --- 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

Ich denke nicht, dass es fair ist, sie zu vergleichen, wie Sie es taten. Beide Funktionen sollten die Unterschiede zu "Deltas" speichern, da die 'using_star_map' im Moment weniger lesbar ist, da sie alle in einer Zeile sind. Ändere es in: 'deltas = starmap (sub, zip (num [1:], num))' 'sum (deltas)/float (len (nums) -1)' – jamylak

+0

@jamylak: Danke, dass du es aufgezeigt hast. Aber leider ändert sich der Leistungsunterschied nicht. – Abhijit

+0

Es war nicht gemeint, aber wir reden auch über Lesbarkeit. – jamylak

Antwort

3

Es ist weitgehend eine Art Sache. Wählen Sie, was Sie besser finden.

In Bezug auf „Es gibt nur einen Weg, es zu tun“, Sven Marnach bietet freundlicherweise diese Guido quote:

„Sie mögen denken, dies verletzt TOOWTDI, aber wie ich schon gesagt habe, dass a war Lüge (auch eine freche Antwort auf Perls Slogan um 2000). Die Möglichkeit, Absicht zum Ausdruck bringt (für den menschlichen Leser) erfordert oft Wahl zwischen mehreren Formen, die im Wesentlichen das gleiche tun, aber den Leser anders aussehen.“

In einer Performance-Hotspot, mögen Sie vielleicht die wählen Lösung, die am schnellsten läuft (was ich in diesem Fall denke, wäre die starmap basierte).

Auf Leistung - Starmap ist langsamer wegen seiner Destrukturierung; jedoch ist starmap hier nicht erforderlich:

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') 

Auf meinem Computer:

>>> 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 kommt schneller ein klein wenig aus, wahrscheinlich, weil es vermeidet Zippen/Destrukturierung.

+0

Aber Zen von Python sagt 'Es sollte einen-- und vorzugsweise nur einen - offensichtlichen Weg geben, es zu tun.' – Abhijit

+0

@Abhijit Ja, aber Sie sollten nicht alles glauben, was Sie lesen. Python hat häufig mehrere gleich gute Möglichkeiten, eine bestimmte Aufgabe zu erfüllen. – Marcin

+0

Aber ist dies nicht die Kernphilosophie des Pythons, die es von "Ruby", "Perl" ... unterscheidet. – Abhijit

10

Der Unterschied, den ich normalerweise sehe, ist map()/starmap() sind am besten geeignet, wo Sie buchstäblich nur eine Funktion für jedes Element in einer Liste aufrufen.In diesem Fall sind sie ein wenig klarer:

(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) 

Sobald Sie in lambda oder filter auch zu werfen beginnen benötigen, sollten Sie auf die Liste comp/Generator Ausdruck wechseln, aber in Fällen, in denen es ein single-Funktion, die Syntax fühlt sich für einen Generator Ausdruck des Listenverständnisses sehr ausführlich an.

Sie sind austauschbar, und wo im Zweifelsfall halten Sie sich an den Generator Ausdruck, wie es besser lesbar im Allgemeinen ist, aber in einem einfachen Fall (map(int, strings), starmap(Vector, points)) mit map()/starmap() kann manchmal Dinge leichter lesbar machen.

Beispiel:

Ein Beispiel, wo ich denke, starmap() ist besser lesbar:

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): 
    ... 

Und für map():

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

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

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

Performance:

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 

Für eine namedtuple Konstruktion:

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 

In meinen Tests, wo wir einfache Funktionen zur Verwendung von (nicht lambda) sprechen, starmap() ist schneller als der entsprechende Generator Ausdruck. Natürlich sollte die Leistung gegenüber der Lesbarkeit vernachlässigt werden, es sei denn, es handelt sich um einen erwiesenen Engpass.

Beispiel dafür, wie lambda tötet jeden Performance-Gewinn, gleiches Beispiel wie im ersten Satz, aber mit lambda statt 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)' ist äquivalent für '[f (x) für x in y]' und nicht für '(f (x) für x in y)', weil es kein Generator ist. Es wird sofort ausgeführt. – akaRem

+1

@akaRem Lattyware verwendet immer Python 3. – Marcin

+0

@akaRem Tut mir leid, ich spreche Python 3.x - in der Tat, in 2.x, das ist wahr. Aktualisiert, um zu klären. –

0

über Starmap .. Hier können Sie L = [(0,1,2),(3,4,5),(6,7,8),..] haben sagen.

Generator comprehansion würde aussehen wie

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

oder

(f(*item) for item in L) 

Und starmap würde aussehen wie

starmap(f, L) 

Die dritte Variante leichter und kürzer ist. Aber das erste ist sehr offensichtlich und es zwingt mich nicht zu dingen, was es tut.

Ok. Jetzt möchte ich noch komplizierter in-line-Code schreiben ..

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

Diese Linie ist nicht offensichtlich, aber immer noch leicht zu verstehen .. In Generator comprehansion es aussehen würde:

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)]) 

Wie Sie sehen, es ist lang, schwer zu verstehen und nicht in einer Linie angeordnet werden könnte, weil es größer als 79 Zeichen ist (PEP 8). Noch kürzere Variante ist schlecht:

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)]) 

Zu viele Zeichen .. Zu viele Klammern .. Zu viel Rauschen.

So. Starmap ist in einigen Fällen ein sehr nützliches Werkzeug. Damit können Sie weniger Code schreiben, der einfacher zu verstehen ist.

EDIT hinzugefügt einige Dummy-Tests

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))") 

Ausgänge (Python 2.7.2)

5.23479851154 
5.35265309689 
4.48601346328 

So starmap ist sogar ~ 15% hier schneller.

+0

Ich würde argumentieren, dass in Ihrem komplexeren Fall sowohl 'starmap()' als auch Generatorausdrücke keine gute Lösung sind. Es ist besser lesbar, Ihren Code (zum Beispiel in einen vollständigen Generator) zu erweitern. –

+0

Ist 'zu viele Geräusche' ein Grammatikfehler oder ist es beabsichtigt? – jamylak

+0

Englisch ist nicht meine Muttersprache .. Ich meine, es gibt viele verschiedene Charaktere, die visuellen Lärm machen. Und deshalb ist dieser Code selbst zum einfachen Lesen schwer (ohne zu versuchen zu verstehen). – akaRem

Verwandte Themen