Czy istnieje powód, aby preferować używanie zamiast listowego rozumowania map()
lub odwrotnie? Czy któryś z nich jest na ogół bardziej wydajny lub uważany za bardziej pytoniczny niż drugi?
python
list-comprehension
map-function
TimothyAWiseman
źródło
źródło
Odpowiedzi:
map
może być mikroskopijnie szybszy w niektórych przypadkach (kiedy NIE robisz lambdy do tego celu, ale używasz tej samej funkcji w mapach i listcomp). Zrozumienie listy może być szybsze w innych przypadkach i większość (nie wszystkie) pythonistów uważa je za bardziej bezpośrednie i jaśniejsze.Przykład małej przewagi mapy przy korzystaniu z dokładnie tej samej funkcji:
Przykład całkowitego odwrócenia porównania wydajności, gdy mapa potrzebuje lambda:
źródło
map(operator.attrgetter('foo'), objs)
łatwiejsze do odczytania niż[o.foo for o in objs]
?!o
tutaj, a twoje przykłady pokazują, dlaczego.str()
przykładzie.Skrzynie
map
, chociaż uważa się ją za „niepythonic”. Na przykładmap(sum, myLists)
jest bardziej elegancki / zwięzły niż[sum(x) for x in myLists]
. Zyskasz elegancję nie konieczności uzupełnić zmienną (np obojętnesum(x) for x...
lubsum(_) for _...
lubsum(readableName) for readableName...
), który trzeba wpisać dwa razy, po prostu iteracyjne. Ten sam argument odnosi się dofilter
areduce
i coś zitertools
modułem: jeśli masz już funkcja przydatna, można iść do przodu i robić niektóre programowania funkcjonalnego. Zyskuje to czytelność w niektórych sytuacjach, a traci ją w innych (np. Początkujący programiści, wiele argumentów) ... ale czytelność twojego kodu w dużej mierze zależy od twoich komentarzy.map
funkcji jako funkcji abstrakcyjnej podczas programowania funkcjonalnego, w którymmap
mapujesz lub currymap
, lub w inny sposób skorzystaj z rozmowymap
jako funkcji. Na przykład w Haskell interfejs funktora o nazwiefmap
uogólnia odwzorowanie na dowolną strukturę danych. Jest to bardzo rzadkie w Pythonie, ponieważ gramatyka pytona zmusza cię do używania stylu generatora do mówienia o iteracji; nie możesz tego łatwo uogólnić. (Czasami jest to dobre, a czasem złe.) Prawdopodobnie możesz wymyślić rzadkie przykłady python, gdziemap(f, *lists)
jest to rozsądne. Najbliższym przykładem, jaki mogę wymyślićsumEach = partial(map,sum)
, jest jeden-liniowiec, który w przybliżeniu odpowiada:for
-loop : Można też oczywiście po prostu użyć pętli for. Choć nie są tak eleganckie z punktu widzenia programowania funkcjonalnego, czasami zmienne nielokalne sprawiają, że kod jest wyraźniejszy w imperatywnych językach programowania, takich jak python, ponieważ ludzie są bardzo przyzwyczajeni do czytania kodu w ten sposób. Pętle For są również na ogół najbardziej wydajne, gdy wykonujesz po prostu jakąkolwiek złożoną operację, która nie buduje listy, takiej jak listy i mapy są zoptymalizowane (np. Sumowanie lub tworzenie drzewa itp.) - przynajmniej wydajna pod względem pamięci (niekoniecznie pod względem czasu, gdzie w najgorszym wypadku spodziewałbym się stałego czynnika, z wyjątkiem rzadkich patologicznych czkawek przy zbieraniu śmieci).„Pythonizm”
Nie podoba mi się słowo „python”, ponieważ nie uważam, że python jest zawsze elegancki w moich oczach. Mimo to,
map
afilter
i podobne funkcje (takie jak bardzo przydatnymitertools
modułem) są prawdopodobnie uważane unpythonic pod względem stylu.Lenistwo
Pod względem wydajności, podobnie jak większość funkcjonalnych konstruktów programistycznych, MAP CAN BE LAZY , aw rzeczywistości jest leniwy w Pythonie. Oznacza to, że możesz to zrobić (w python3 ), a komputerowi nie zabraknie pamięci i nie stracisz wszystkich niezapisanych danych:
Spróbuj to zrobić ze zrozumieniem listy:
Zauważ, że rozumienie list jest z natury leniwe, ale Python zdecydował się je zaimplementować jako nieleniwe . Niemniej jednak Python obsługuje wyrażenia leniwych list w postaci wyrażeń generatora, jak następuje:
Zasadniczo można myśleć o
[...]
składni jako przekazywaniu wyrażenia generatora do konstruktora listy, takiego jaklist(x for x in range(5))
.Krótki wymyślony przykład
Rozumienie listy nie jest leniwe, więc może wymagać więcej pamięci (chyba że używasz generatora). Nawiasy kwadratowe
[...]
często sprawiają, że wszystko staje się oczywiste, szczególnie gdy są w bałaganie w nawiasach. Z drugiej strony, czasami kończysz się na mówieniu jak pisanie[x for x in...
. Tak długo, jak długo zmienne iteratora są krótkie, opisy na liście są zwykle wyraźniejsze, jeśli nie wcinasz kodu. Ale zawsze możesz wciąć swój kod.lub rozbijać rzeczy:
Porównanie wydajności dla python3
map
jest teraz leniwy:Dlatego jeśli nie będziesz używać wszystkich swoich danych lub nie wiesz z góry, ile danych potrzebujesz,
map
w python3 (i wyrażeniach generatora w python2 lub python3) unikniesz obliczania ich wartości do ostatniej niezbędnej chwili. Zwykle przeważa to zwykle nad kosztami użytkowaniamap
. Minusem jest to, że jest to bardzo ograniczone w Pythonie, w przeciwieństwie do większości języków funkcjonalnych: tę korzyść uzyskuje się tylko wtedy, gdy uzyskuje się dostęp do danych od lewej do prawej „w kolejności”, ponieważ wyrażenia generatora Pythona można oceniać tylko w kolejnościx[0], x[1], x[2], ...
.Powiedzmy jednak, że mamy gotową funkcję
f
, którą chcielibyśmymap
, i ignorujemy lenistwomap
, natychmiast zmuszając do ocenylist(...)
. Otrzymujemy bardzo interesujące wyniki:W rezultacie są w postaci AAA / BBB / CCC, gdzie A wykonano na stacji roboczej Intel około 2010 z pythonem 3.?.?, A B i C wykonano na stacji roboczej AMD około 2013 z pythonem 3.2.1, z bardzo innym sprzętem. W rezultacie wydaje się, że wyniki map i list są porównywalne pod względem wydajności, na co największy wpływ mają inne czynniki losowe. Wydaje się, że jedyne, co możemy powiedzieć, to to, że, o dziwo, podczas gdy spodziewamy się, że
[...]
interpretacje list będą działały lepiej niż wyrażenia generatora(...)
,map
ODPOWIADAJĄ RÓWNIEŻ bardziej niż wyrażeniom generatora (ponownie zakładając, że wszystkie wartości są oceniane / używane).Ważne jest, aby zdawać sobie sprawę, że testy te przyjmują bardzo prostą funkcję (funkcję tożsamości); jest to jednak w porządku, ponieważ gdyby funkcja była skomplikowana, wówczas narzut wydajności byłby znikomy w porównaniu z innymi czynnikami w programie. (Testowanie przy użyciu innych prostych rzeczy, takich jak
f=lambda x:x+x
) może być interesująceJeśli jesteś biegły w czytaniu zestawu Python, możesz użyć
dis
modułu, aby sprawdzić, czy tak naprawdę dzieje się za kulisami:Wydaje się, że lepiej jest używać
[...]
składni niżlist(...)
. Niestetymap
klasa jest nieco nieprzejrzysta do demontażu, ale możemy to zrobić dzięki naszemu testowi prędkości.źródło
map
ifilter
wraz ze standardową bibliotekąitertools
są z natury złe. O ile GvR nie powie, że to był albo straszny błąd, albo tylko wydajność, jedynym naturalnym wnioskiem, jeśli tak mówi „Pythonicness”, jest zapomnienie o tym, że jest głupie ;-)map
filter
, że upuszczenie / to świetny pomysł na Python 3 , a tylko bunt innych Pythonistów utrzymywał je we wbudowanej przestrzeni nazw (podczas gdyreduce
został przeniesiony dofunctools
). Ja osobiście się nie zgadzam (map
i nie mam nic przeciwkofilter
wstępnie zdefiniowanym, szczególnie wbudowanym funkcjom, po prostu nigdy ich nie używam, jeślilambda
byłoby to potrzebne), ale GvR od lat nazywa je zasadniczo nie Pythonicami.itertools
? Część, którą cytuję w tej odpowiedzi, jest głównym twierdzeniem, które mnie otacza. Nie wiem, czy w jego idealnym świeciemap
ifilter
przeniósłby się doitertools
(lubfunctools
), czy też poszedłby całkowicie, ale cokolwiek by to nie było, kiedy ktoś powie, żeitertools
jest to w ogóle nie mityczne, to tak naprawdę nie wiem, czym jest „Pythonic” powinno to znaczyć, ale nie sądzę, że może to być coś podobnego do „tego, co GvR zaleca ludziom”.map
/filter
, nieitertools
. Programowanie funkcjonalne doskonale pythonowy (itertools
,functools
ioperator
zostały zaprojektowane specjalnie z programowania funkcyjnego w umyśle i używam funkcjonalne idiomów w Pythonie cały czas), iitertools
zapewnia funkcje, które byłyby uciążliwe wdrożyć samemu, to specjalniemap
ifilter
jest zbędny z wyrażeniami prądotwórczych przez co Guido ich nienawidzi.itertools
zawsze było dobrze.Python 2: Powinieneś używać
map
ifilter
zamiast wyrażeń listowych.Obiektywny powód, dlaczego wolisz je, chociaż nie są one „pythonowy” jest taka:
Wymagają one funkcje / lambdy jako argumenty, które wprowadzają nowe możliwości .
Ugryzło mnie to nie raz:
ale jeśli zamiast tego powiedziałbym:
wtedy wszystko byłoby dobrze.
Można powiedzieć, że byłem głupi, że używałem tej samej nazwy zmiennej w tym samym zakresie.
Nie byłem Kod był początkowo w porządku - dwa
x
nie były w tym samym zakresie.Dopiero po przeniesieniu wewnętrznego bloku do innej sekcji kodu pojawił się problem (czytaj: problem podczas konserwacji, a nie programowania) i nie spodziewałem się tego.
Tak, jeśli nigdy nie popełnisz tego błędu, lista wyrażeń będzie bardziej elegancka.
Ale z własnego doświadczenia (i widząc, że inni popełniają ten sam błąd) widziałem, że zdarzało się to wystarczająco często, że myślę, że nie jest to warte bólu, przez który musisz przejść, gdy te błędy wkradają się do twojego kodu.
Wniosek:
Użyj
map
ifilter
. Zapobiegają subtelnym, trudnym do zdiagnozowania błędom związanym z zasięgiem.Dygresja:
Nie zapomnij rozważyć użycia
imap
iifilter
(initertools
), jeśli są odpowiednie dla twojej sytuacji!źródło
map
i / lubfilter
. Jeśli cokolwiek, najbardziej bezpośrednim i logicznym tłumaczeniem, aby uniknąć problemu, jest niemap(lambda x: x ** 2, numbers)
wyrażenie generatora,list(x ** 2 for x in numbers)
które nie przecieka, jak już zauważył JeromeJ. Spójrz Mehrdad, nie bierz głosowania tak osobiście, po prostu zdecydowanie nie zgadzam się z twoim rozumowaniem tutaj.W rzeczywistości
map
wyrazy listowe zachowują się zupełnie inaczej w języku Python 3. Spójrz na następujący program Python 3:Można się spodziewać, że wypisze dwukrotnie wiersz „[1, 4, 9]”, ale zamiast tego wypisze „[1, 4, 9]”, a następnie „[]”. Za pierwszym razem wygląda na
squares
to, że zachowuje się jak sekwencja trzech elementów, ale za drugim razem jest pusta.W języku Python 2
map
zwraca zwykłą, starą listę, podobnie jak w przypadku obu języków. Najważniejsze jest to, że zwracana wartośćmap
w Pythonie 3 (iimap
Pythonie 2) nie jest listą - to iterator!Elementy są zużywane podczas iteracji w iteratorze, w przeciwieństwie do iteracji w liście. To dlatego
squares
wygląda pusto w ostatnimprint(list(squares))
wierszu.Podsumowując:
źródło
map
stworzyć strukturę danych, a nie iterator. Ale może leniwe iteratory są łatwiejsze niż leniwe struktury danych. Jedzenie do namysłu. Dzięki @MnZrKUważam, że rozumienie list jest ogólnie bardziej wyraziste w stosunku do tego, co próbuję zrobić, niż
map
- oboje to robią, ale ten pierwszy oszczędza obciążenia psychicznego związanego z próbą zrozumienia, co może być złożonymlambda
wyrażeniem.Jest też gdzieś wywiad (nie mogę go znaleźć od razu), w którym Guido wymienia
lambda
s i funkcje funkcjonalne jako rzecz, której najbardziej żałuje, przyjmując do Pythona, abyś mógł argumentować, że są nie Pythoniczni z powodu tego.źródło
const
słowo kluczowe w C ++ jest wielkim triumfem w tych liniach.lambda
zostały tak ułomne (brak stwierdzeń ...), że są trudne w użyciu i ograniczone.Oto jeden możliwy przypadek:
przeciw:
Domyślam się, że zip () jest niefortunnym i niepotrzebnym narzutem, na który musisz sobie pozwolić, jeśli nalegasz na używanie list ze zrozumieniem zamiast mapy. Byłoby wspaniale, gdyby ktoś wyjaśnił to twierdząco lub negatywnie.
źródło
zip
lenistwo, używającitertools.izip
map(operator.mul, list1, list2)
. Na tych bardzo prostych wyrażeniach po lewej stronie nieporozumienia stają się niezdarne.Jeśli planujesz napisać kod asynchroniczny, równoległy lub rozproszony, prawdopodobnie wolisz
map
od zrozumienia listy - większość pakietów asynchronicznych, równoległych lub rozproszonych udostępniamap
funkcję do przeciążania pytonamap
. Następnie przekazując odpowiedniąmap
funkcję do reszty kodu, może nie być konieczne modyfikowanie oryginalnego kodu seryjnego, aby działał równolegle (itp.).źródło
Ponieważ Python 3
map()
jest iteratorem, musisz pamiętać o tym, czego potrzebujesz: iteratorze lublist
obiekcie.Jak już wspomniano @AlexMartelli ,
map()
jest szybsze niż zrozumienie listy tylko wtedy, gdy nie używaszlambda
funkcji.Przedstawię wam porównania czasu.
Python 3.5.2 i CPython
Użyłem notatnika Jupiter, a szczególnie
%timeit
wbudowanej magicznej komendyPomiary : s == 1000 ms == 1000 * 1000 µs = 1000 * 1000 * 1000 ns
Ustawiać:
Wbudowana funkcja:
lambda
funkcjonować:Istnieje również coś takiego jak wyrażenie generatora, patrz PEP-0289 . Pomyślałem więc, że przydałoby się dodać to do porównania
Potrzebujesz
list
obiektu:Użyj rozumienia listy, jeśli jest to funkcja niestandardowa, użyj
list(map())
jeśli jest wbudowana funkcjaNie potrzebujesz
list
obiektu, wystarczy iterowalny:Zawsze używaj
map()
!źródło
Przeprowadziłem szybki test porównujący trzy metody wywoływania metody obiektu. Różnica czasu, w tym przypadku, jest znikoma i jest sprawą funkcji w pytaniu (zob @Alex martelli za odpowiedź ). Tutaj spojrzałem na następujące metody:
Spojrzałem na listy (przechowywane w zmiennej
vals
) zarówno liczb całkowitych (Pythonint
), jak i liczb zmiennoprzecinkowych (Pythonfloat
) w celu zwiększenia rozmiarów list. Uwzględnia się następującą klasę manekinaDummyNum
:W szczególności
add
metoda.__slots__
Cechą jest prosta optymalizacja Pythonie określić całkowitą ilość pamięci potrzebnej do klasy (cech), zmniejszając rozmiar pamięci. Oto wynikowe wykresy.Jak wspomniano wcześniej, zastosowana technika stanowi minimalną różnicę i powinieneś kodować w sposób najbardziej czytelny dla Ciebie lub w szczególnych okolicznościach. W takim przypadku zrozumienie listy (
map_comprehension
technika) jest najszybsze dla obu typów dodatków w obiekcie, szczególnie w przypadku krótszych list.Odwiedź tę pastebin, aby znaleźć źródło użyte do wygenerowania wykresu i danych.
źródło
map
jest szybszy tylko wtedy, gdy funkcja jest wywoływana dokładnie w ten sam sposób (tj.[*map(f, vals)]
Vs.[f(x) for x in vals]
). Więclist(map(methodcaller("add"), vals))
jest szybszy niż[methodcaller("add")(x) for x in vals]
.map
może nie być szybszy, gdy zapętlony odpowiednik używa innej metody wywoływania, która może uniknąć pewnego narzutu (np.x.add()
pozwala uniknąć narzutumethodcaller
lub wyrażenia lambda). W tym konkretnym przypadku testowym[*map(DummyNum.add, vals)]
byłoby szybsze (ponieważDummyNum.add(x)
ix.add()
zasadniczo mają taką samą wydajność).list()
połączenia są nieco wolniejsze niż rozumienie listy. Dla uczciwego porównania musisz napisać[*map(...)]
.list()
połączenia zwiększają koszty ogólne. Powinienem był poświęcić więcej czasu na czytanie odpowiedzi. Ponownie przeprowadzę te testy, aby uzyskać rzetelne porównanie, bez względu na to, jak niewielkie mogą być różnice.Uważam, że najbardziej pythonicznym sposobem jest użycie rozumienia listy zamiast
map
ifilter
. Powodem jest to, że listy są bardziej zrozumiałe niżmap
ifilter
.Jak widzisz, zrozumienie nie wymaga dodatkowych
lambda
wyrażeń w raziemap
potrzeby. Co więcej, zrozumienie pozwala również na łatwe filtrowanie, podczas gdymap
wymagafilter
zezwolenia na filtrowanie.źródło
Próbowałem kodu przez @ alex-martelli, ale znalazłem pewne rozbieżności
mapa zajmuje tyle samo czasu, nawet w przypadku bardzo dużych zasięgów, podczas gdy korzystanie ze zrozumienia listy zajmuje dużo czasu, jak wynika z mojego kodu. Więc poza tym, że uważano mnie za „nie-mitycznego”, nie spotkałem się z żadnymi problemami z wydajnością związanymi z korzystaniem z mapy.
źródło
map
zwraca listę. W Pythonie 3map
jest leniwie oceniany, więc po prostu wywołaniemap
nie oblicza żadnego z nowych elementów listy, dlatego otrzymujesz tak krótkie czasy.