Zrozumienie listy a mapa

732

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?

TimothyAWiseman
źródło
8
Zauważ, że PyLint ostrzega, jeśli używasz mapy zamiast listy, zobacz komunikat W0141 .
lędźwiowy
2
@lumbric, nie jestem pewien, ale dzieje się tak tylko wtedy, gdy lambda jest używana w mapie.
0xc0de

Odpowiedzi:

660

mapmoż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:

$ python -mtimeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop
$ python -mtimeit -s'xs=range(10)' '[hex(x) for x in xs]'
100000 loops, best of 3: 5.58 usec per loop

Przykład całkowitego odwrócenia porównania wydajności, gdy mapa potrzebuje lambda:

$ python -mtimeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop
$ python -mtimeit -s'xs=range(10)' '[x+2 for x in xs]'
100000 loops, best of 3: 2.32 usec per loop
Alex Martelli
źródło
39
Tak, w rzeczy samej nasz wewnętrzny przewodnik po stylu Pythona wyraźnie poleca listcomps w stosunku do mapy i filtra (nawet nie wspominając o małej, ale mierzalnej mapie poprawy wydajności w niektórych przypadkach ;-).
Alex Martelli,
46
Nie kibash na nieskończonych punktach stylu Alexa, ale czasami mapa wydaje mi się łatwiejsza do odczytania: data = map (str, some_list_of_objects). Niektóre inne ... operator.attrgetter, operator.itemgetter itp.
Gregg Lind
57
map(operator.attrgetter('foo'), objs)łatwiejsze do odczytania niż [o.foo for o in objs]?!
Alex Martelli,
52
@Alex: Wolę nie wprowadzać niepotrzebnych nazw, jak otutaj, a twoje przykłady pokazują, dlaczego.
Reid Barton
29
Myślę jednak, że @GreggLind ma rację w tym str()przykładzie.
Eric O Lebigot,
474

Skrzynie

  • Typowy przypadek : prawie zawsze będziesz chciał używać listy w Pythonie, ponieważ bardziej oczywiste będzie, co robisz początkującym programistom czytającym Twój kod. (Nie dotyczy to innych języków, w których mogą obowiązywać inne idiomy.) Będzie jeszcze bardziej oczywiste, co robisz programistom pythonowym, ponieważ rozumienie list jest de facto standardem w pythonie do iteracji; są oczekiwane .
  • Rzadziej spotykany przypadek : jeśli jednak masz już zdefiniowaną funkcję , jej użycie jest często uzasadnione map, chociaż uważa się ją za „niepythonic”. Na przykład map(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ętne sum(x) for x...lub sum(_) for _...lub sum(readableName) for readableName...), który trzeba wpisać dwa razy, po prostu iteracyjne. Ten sam argument odnosi się do filtera reducei coś z itertoolsmoduł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.
  • Prawie nigdy : możesz użyć tej mapfunkcji jako funkcji abstrakcyjnej podczas programowania funkcjonalnego, w którym mapmapujesz lub curry map, lub w inny sposób skorzystaj z rozmowy mapjako funkcji. Na przykład w Haskell interfejs funktora o nazwie fmapuogó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, gdzie map(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:

def sumEach(myLists):
    return [sum(_) for _ in myLists]
  • Wystarczy za pomocą 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, mapa filteri podobne funkcje (takie jak bardzo przydatnym itertoolsmoduł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:

>>> map(str, range(10**100))
<map object at 0x2201d50>

Spróbuj to zrobić ze zrozumieniem listy:

>>> [str(n) for n in range(10**100)]
# DO NOT TRY THIS AT HOME OR YOU WILL BE SAD #

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:

>>> (str(n) for n in range(10**100))
<generator object <genexpr> at 0xacbdef>

Zasadniczo można myśleć o [...]składni jako przekazywaniu wyrażenia generatora do konstruktora listy, takiego jak list(x for x in range(5)).

Krótki wymyślony przykład

from operator import neg
print({x:x**2 for x in map(neg,range(5))})

print({x:x**2 for x in [-y for y in range(5)]})

print({x:x**2 for x in (-y for y in range(5))})

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.

print(
    {x:x**2 for x in (-y for y in range(5))}
)

lub rozbijać rzeczy:

rangeNeg5 = (-y for y in range(5))
print(
    {x:x**2 for x in rangeNeg5}
)

Porównanie wydajności dla python3

map jest teraz leniwy:

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=map(f,xs)'
1000000 loops, best of 3: 0.336 usec per loop            ^^^^^^^^^

Dlatego jeśli nie będziesz używać wszystkich swoich danych lub nie wiesz z góry, ile danych potrzebujesz, mapw 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żytkowania map. 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ści x[0], x[1], x[2], ....

Powiedzmy jednak, że mamy gotową funkcję f, którą chcielibyśmy map, i ignorujemy lenistwo map, natychmiast zmuszając do oceny list(...). Otrzymujemy bardzo interesujące wyniki:

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(map(f,xs))'                                                                                                                                                
10000 loops, best of 3: 165/124/135 usec per loop        ^^^^^^^^^^^^^^^
                    for list(<map object>)

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=[f(x) for x in xs]'                                                                                                                                      
10000 loops, best of 3: 181/118/123 usec per loop        ^^^^^^^^^^^^^^^^^^
                    for list(<generator>), probably optimized

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(f(x) for x in xs)'                                                                                                                                    
1000 loops, best of 3: 215/150/150 usec per loop         ^^^^^^^^^^^^^^^^^^^^^^
                    for list(<generator>)

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 (...), mapODPOWIADAJĄ 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ące

Jeśli jesteś biegły w czytaniu zestawu Python, możesz użyć dismodułu, aby sprawdzić, czy tak naprawdę dzieje się za kulisami:

>>> listComp = compile('[f(x) for x in xs]', 'listComp', 'eval')
>>> dis.dis(listComp)
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x2511a48, file "listComp", line 1>) 
              3 MAKE_FUNCTION            0 
              6 LOAD_NAME                0 (xs) 
              9 GET_ITER             
             10 CALL_FUNCTION            1 
             13 RETURN_VALUE         
>>> listComp.co_consts
(<code object <listcomp> at 0x2511a48, file "listComp", line 1>,)
>>> dis.dis(listComp.co_consts[0])
  1           0 BUILD_LIST               0 
              3 LOAD_FAST                0 (.0) 
        >>    6 FOR_ITER                18 (to 27) 
              9 STORE_FAST               1 (x) 
             12 LOAD_GLOBAL              0 (f) 
             15 LOAD_FAST                1 (x) 
             18 CALL_FUNCTION            1 
             21 LIST_APPEND              2 
             24 JUMP_ABSOLUTE            6 
        >>   27 RETURN_VALUE

 

>>> listComp2 = compile('list(f(x) for x in xs)', 'listComp2', 'eval')
>>> dis.dis(listComp2)
  1           0 LOAD_NAME                0 (list) 
              3 LOAD_CONST               0 (<code object <genexpr> at 0x255bc68, file "listComp2", line 1>) 
              6 MAKE_FUNCTION            0 
              9 LOAD_NAME                1 (xs) 
             12 GET_ITER             
             13 CALL_FUNCTION            1 
             16 CALL_FUNCTION            1 
             19 RETURN_VALUE         
>>> listComp2.co_consts
(<code object <genexpr> at 0x255bc68, file "listComp2", line 1>,)
>>> dis.dis(listComp2.co_consts[0])
  1           0 LOAD_FAST                0 (.0) 
        >>    3 FOR_ITER                17 (to 23) 
              6 STORE_FAST               1 (x) 
              9 LOAD_GLOBAL              0 (f) 
             12 LOAD_FAST                1 (x) 
             15 CALL_FUNCTION            1 
             18 YIELD_VALUE          
             19 POP_TOP              
             20 JUMP_ABSOLUTE            3 
        >>   23 LOAD_CONST               0 (None) 
             26 RETURN_VALUE

 

>>> evalledMap = compile('list(map(f,xs))', 'evalledMap', 'eval')
>>> dis.dis(evalledMap)
  1           0 LOAD_NAME                0 (list) 
              3 LOAD_NAME                1 (map) 
              6 LOAD_NAME                2 (f) 
              9 LOAD_NAME                3 (xs) 
             12 CALL_FUNCTION            2 
             15 CALL_FUNCTION            1 
             18 RETURN_VALUE 

Wydaje się, że lepiej jest używać [...]składni niż list(...). Niestety mapklasa jest nieco nieprzejrzysta do demontażu, ale możemy to zrobić dzięki naszemu testowi prędkości.

ninjagecko
źródło
5
„bardzo przydatny moduł itertools [prawdopodobnie] jest uważany za mało mityczny pod względem stylu”. Hmm Nie podoba mi się też termin „Python”, więc w pewnym sensie nie dbam o to, co to znaczy, ale nie sądzę, aby było uczciwe wobec tych, którzy go używają, aby powiedzieć, że zgodnie z wbudowanymi „Pythonicness” mapi filterwraz ze standardową biblioteką itertoolssą 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 ;-)
Steve Jessop
4
@ SteveJessop: W rzeczywistości Guido pomyślałmapfilter , że upuszczenie / to świetny pomysł na Python 3 , a tylko bunt innych Pythonistów utrzymywał je we wbudowanej przestrzeni nazw (podczas gdy reducezostał przeniesiony do functools). Ja osobiście się nie zgadzam ( mapi nie mam nic przeciwko filterwstępnie zdefiniowanym, szczególnie wbudowanym funkcjom, po prostu nigdy ich nie używam, jeśli lambdabyłoby to potrzebne), ale GvR od lat nazywa je zasadniczo nie Pythonicami.
ShadowRanger,
@ShadowRanger: prawda, ale czy GvR kiedykolwiek planował usunąć itertools? Część, którą cytuję w tej odpowiedzi, jest głównym twierdzeniem, które mnie otacza. Nie wiem, czy w jego idealnym świecie mapi filterprzeniósłby się do itertools(lub functools), czy też poszedłby całkowicie, ale cokolwiek by to nie było, kiedy ktoś powie, że itertoolsjest 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”.
Steve Jessop
2
@ SteveJessop: Mówiłem tylko map/ filter, nie itertools. Programowanie funkcjonalne doskonale pythonowy ( itertools, functoolsi operatorzostały zaprojektowane specjalnie z programowania funkcyjnego w umyśle i używam funkcjonalne idiomów w Pythonie cały czas), i itertoolszapewnia funkcje, które byłyby uciążliwe wdrożyć samemu, to specjalnie mapi filterjest zbędny z wyrażeniami prądotwórczych przez co Guido ich nienawidzi. itertoolszawsze było dobrze.
ShadowRanger,
1
Mógłbym faworyzować tę odpowiedź, gdyby istniał sposób. Dobrze wyjaśnione.
NelsonGon
95

Python 2: Powinieneś używać mapi filterzamiast 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:

for x, y in somePoints:
    # (several lines of code here)
    squared = [x ** 2 for x in numbers]
    # Oops, x was silently overwritten!

ale jeśli zamiast tego powiedziałbym:

for x, y in somePoints:
    # (several lines of code here)
    squared = map(lambda x: x ** 2, numbers)

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 xnie 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 mapi filter. Zapobiegają subtelnym, trudnym do zdiagnozowania błędom związanym z zasięgiem.

Dygresja:

Nie zapomnij rozważyć użycia imapi ifilter(in itertools), jeśli są odpowiednie dla twojej sytuacji!

użytkownik541686
źródło
7
Dzięki za zwrócenie na to uwagi. Nie przyszło mi do głowy, że zrozumienie listy ma ten sam zakres i może stanowić problem. Biorąc to pod uwagę, myślę, że niektóre inne odpowiedzi wyjaśniają, że zrozumienie listy powinno być domyślnym podejściem przez większość czasu, ale należy o tym pamiętać. Jest to również dobre ogólne przypomnienie o tym, aby funkcje (a tym samym zakres) były małe, mieć dokładne testy jednostkowe i używać instrukcji aser.
TimothyAWiseman
13
@ wim: Chodziło tylko o Python 2, chociaż dotyczy Python 3, jeśli chcesz pozostać kompatybilny wstecz. Wiedziałem o tym i używałem Pythona od dłuższego czasu (tak, ponad kilka miesięcy), a jednak zdarzyło mi się to. Widziałem innych mądrzejszych ode mnie, wpadających w tę samą pułapkę. Jeśli jesteś tak bystry i / lub doświadczony, że nie stanowi to dla ciebie problemu, cieszę się z ciebie, nie sądzę, że większość ludzi jest taka jak ty. Gdyby tak było, nie byłoby takiej potrzeby naprawienia go w Pythonie 3.
user541686
12
Przykro mi, ale napisałeś to pod koniec 2012 r., Długo po pojawieniu się Pythona 3, a odpowiedź brzmi, jakbyś polecał inny, niepopularny styl kodowania w Pythonie tylko dlatego, że ugryzł cię błąd podczas cięcia i wklejanie kodu. Nigdy nie twierdziłem, że jestem bystry lub doświadczony, po prostu nie zgadzam się, że śmiałe twierdzenie jest uzasadnione twoimi powodami.
wim
8
@wim: Huh? Python 2 jest nadal używany w wielu miejscach, fakt, że Python 3 istnieje, nie zmienia tego. A kiedy powiesz „to nie jest subtelny błąd dla każdego, kto używa Pythona od kilku miesięcy”, to zdanie dosłownie oznacza „dotyczy to tylko niedoświadczonych programistów” (oczywiście nie ty). Dla przypomnienia, wyraźnie nie przeczytałeś odpowiedzi, ponieważ odważnie powiedziałem , że poruszam , a nie kopiuję, kod. Błędy kopiowania i wklejania są dość jednolite we wszystkich językach. Ten rodzaj błędu jest bardziej unikalny dla Pythona ze względu na jego zasięg; jest subtelniejszy i łatwiej go zapomnieć i przegapić.
user541686,
3
Nadal nie jest to logiczny powód przejścia na mapi / lub filter. Jeśli cokolwiek, najbardziej bezpośrednim i logicznym tłumaczeniem, aby uniknąć problemu, jest nie map(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.
wim
46

W rzeczywistości mapwyrazy listowe zachowują się zupełnie inaczej w języku Python 3. Spójrz na następujący program Python 3:

def square(x):
    return x*x
squares = map(square, [1, 2, 3])
print(list(squares))
print(list(squares))

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 squaresto, ż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ść mapw Pythonie 3 (i imapPythonie 2) nie jest listą - to iterator!

Elementy są zużywane podczas iteracji w iteratorze, w przeciwieństwie do iteracji w liście. To dlatego squareswygląda pusto w ostatnim print(list(squares))wierszu.

Podsumowując:

  • Kiedy masz do czynienia z iteratorami, musisz pamiętać, że są one stanowe i że mutują, gdy je przemierzasz.
  • Listy są bardziej przewidywalne, ponieważ zmieniają się tylko wtedy, gdy zostaną jawnie zmutowane; to są pojemniki .
  • I bonus: liczby, łańcuchy i krotki są jeszcze bardziej przewidywalne, ponieważ w ogóle nie mogą się zmienić; są wartościami .
raek
źródło
jest to prawdopodobnie najlepszy argument na zrozumienie listy. mapa pytonów nie jest mapą funkcjonalną, ale okaleczonym pasierbem o rudowłosej implementacji funkcjonalnej. Bardzo smutne, bo naprawdę nie lubię rozumienia.
semiomant
@semiomant Powiedziałbym, że leniwa mapa (jak w python3) jest bardziej „funkcjonalna” niż chętna mapa (jak w python2). Na przykład mapa w Haskell jest leniwa (cóż, wszystko w Haskell jest leniwe ...). W każdym razie leniwa mapa jest lepsza do tworzenia łańcuchów map - jeśli masz mapę zastosowaną do mapy zastosowanej do mapy, masz listę dla każdego z pośrednich wywołań mapy w python2, podczas gdy w python3 masz tylko jedną wynikową listę, więc jej bardziej wydajna pamięć .
MnZrK
Wydaje mi się, że chcę mapstworzyć strukturę danych, a nie iterator. Ale może leniwe iteratory są łatwiejsze niż leniwe struktury danych. Jedzenie do namysłu. Dzięki @MnZrK
semiomant
Chcesz powiedzieć, że mapa zwraca iterowalny, a nie iteracyjny.
user541686,
16

Uważ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żonym lambdawyrażeniem.

Jest też gdzieś wywiad (nie mogę go znaleźć od razu), w którym Guido wymienia lambdas 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.

Dan
źródło
9
Tak, westchnienie, ale pierwotny zamiar Guido, aby całkowicie usunąć lambdę w Pythonie 3, wywołał falę lobbingu przeciwko temu, więc wrócił do niego pomimo mojego zdecydowanego wsparcia - no cóż, zgadnij, że lambda jest po prostu zbyt przydatna w wielu PROSTYCH przypadkach, jedynym Problem polega na tym, że przekracza granice SIMPLE lub zostaje przypisany do nazwy (w tym drugim przypadku jest to głupia, pokręcona kopia def! -).
Alex Martelli,
1
Wywiad, o którym myślisz, jest następujący: amk.ca/python/writing/gvr-interview , w którym Guido mówi: „Czasami zbyt szybko przyjmowałem datki, a później zdałem sobie sprawę, że to pomyłka. Jednym z przykładów może być niektóre funkcje programowania funkcjonalnego, takie jak funkcje lambda. lambda jest słowem kluczowym, które pozwala utworzyć małą anonimową funkcję; wbudowane funkcje, takie jak mapowanie, filtrowanie i zmniejszanie, uruchamiają funkcję nad typem sekwencji, takim jak lista. „
J. Taylor,
3
@Alex, nie mam twojego wieloletniego doświadczenia, ale widziałem o wiele bardziej skomplikowane rozumienie list niż lambdas. Oczywiście nadużywanie funkcji językowych jest zawsze trudną pokusą do oparcia się. Interesujące jest to, że rozumienia list (empirycznie) wydają się bardziej podatne na nadużycia niż lambdas, chociaż nie jestem pewien, dlaczego tak powinno być. Zwrócę też uwagę, że „kuśtykanie” nie zawsze jest złą rzeczą. Ograniczenie zakresu „rzeczy, które ta linia może robić” może czasem ułatwić czytelnikowi. Na przykład constsłowo kluczowe w C ++ jest wielkim triumfem w tych liniach.
Stuart Berg
> guido. Co jest kolejnym dowodem na to, że Guido oszalał. Oczywiście lambdazostały tak ułomne (brak stwierdzeń ...), że są trudne w użyciu i ograniczone.
javadba
16

Oto jeden możliwy przypadek:

map(lambda op1,op2: op1*op2, list1, list2)

przeciw:

[op1*op2 for op1,op2 in zip(list1,list2)]

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.

Andz
źródło
„[op1 * op2 z op1, op2 w zipie (lista 1, lista 2)]” s / form / for / I ekwiwalentna lista bez zip: (mniej czytelna) [list1 [i] * list2 [i] dla i in range (len (list1))]
słaba
2
Powinien być „za” nie ”z” w drugim cytacie kodu, @andz, a także w komentarzu @ slaish. Myślałem, że odkryłem nowe podejście syntaktyczne do listowego rozumienia ... Darn.
physicsmichael
4
aby dodać bardzo późny komentarz, możesz zrobić ziplenistwo, używającitertools.izip
tacaswell
5
Myślę, że nadal wolę map(operator.mul, list1, list2). Na tych bardzo prostych wyrażeniach po lewej stronie nieporozumienia stają się niezdarne.
Yann Vernier
1
Nie zdawałem sobie sprawy, że mapa może przyjmować kilka iteracji jako dane wejściowe dla swojej funkcji, dzięki czemu mogę uniknąć zip.
bli
16

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ępnia mapfunkcję do przeciążania pytona map. Następnie przekazując odpowiednią mapfunkcję do reszty kodu, może nie być konieczne modyfikowanie oryginalnego kodu seryjnego, aby działał równolegle (itp.).

Mike McKerns
źródło
9

Ponieważ Python 3 map()jest iteratorem, musisz pamiętać o tym, czego potrzebujesz: iteratorze lub listobiekcie.

Jak już wspomniano @AlexMartelli , map()jest szybsze niż zrozumienie listy tylko wtedy, gdy nie używasz lambdafunkcji.

Przedstawię wam porównania czasu.

Python 3.5.2 i CPython
Użyłem notatnika Jupiter, a szczególnie %timeitwbudowanej magicznej komendy
Pomiary : s == 1000 ms == 1000 * 1000 µs = 1000 * 1000 * 1000 ns

Ustawiać:

x_list = [(i, i+1, i+2, i*2, i-9) for i in range(1000)]
i_list = list(range(1000))

Wbudowana funkcja:

%timeit map(sum, x_list)  # creating iterator object
# Output: The slowest run took 9.91 times longer than the fastest. 
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 277 ns per loop

%timeit list(map(sum, x_list))  # creating list with map
# Output: 1000 loops, best of 3: 214 µs per loop

%timeit [sum(x) for x in x_list]  # creating list with list comprehension
# Output: 1000 loops, best of 3: 290 µs per loop

lambda funkcjonować:

%timeit map(lambda i: i+1, i_list)
# Output: The slowest run took 8.64 times longer than the fastest. 
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 325 ns per loop

%timeit list(map(lambda i: i+1, i_list))
# Output: 1000 loops, best of 3: 183 µs per loop

%timeit [i+1 for i in i_list]
# Output: 10000 loops, best of 3: 84.2 µs per loop

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

%timeit (sum(i) for i in x_list)
# Output: The slowest run took 6.66 times longer than the fastest. 
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 495 ns per loop

%timeit list((sum(x) for x in x_list))
# Output: 1000 loops, best of 3: 319 µs per loop

%timeit (i+1 for i in i_list)
# Output: The slowest run took 6.83 times longer than the fastest. 
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 506 ns per loop

%timeit list((i+1 for i in i_list))
# Output: 10000 loops, best of 3: 125 µs per loop

Potrzebujesz list obiektu:

Użyj rozumienia listy, jeśli jest to funkcja niestandardowa, użyj list(map()) jeśli jest wbudowana funkcja

Nie potrzebujesz list obiektu, wystarczy iterowalny:

Zawsze używaj map()!

vishes_shell
źródło
1

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:

# map_lambda
list(map(lambda x: x.add(), vals))

# map_operator
from operator import methodcaller
list(map(methodcaller("add"), vals))

# map_comprehension
[x.add() for x in vals]

Spojrzałem na listy (przechowywane w zmiennej vals) zarówno liczb całkowitych (Python int), jak i liczb zmiennoprzecinkowych (Python float) w celu zwiększenia rozmiarów list. Uwzględnia się następującą klasę manekina DummyNum:

class DummyNum(object):
    """Dummy class"""
    __slots__ = 'n',

    def __init__(self, n):
        self.n = n

    def add(self):
        self.n += 5

W szczególności addmetoda. __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.

Wydajność mapowania metod obiektów Python

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.

Craymichael
źródło
1
Jak już wyjaśniono w innych odpowiedziach, mapjest 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ęc list(map(methodcaller("add"), vals))jest szybszy niż [methodcaller("add")(x) for x in vals]. mapmoż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ąć narzutu methodcallerlub wyrażenia lambda). W tym konkretnym przypadku testowym [*map(DummyNum.add, vals)]byłoby szybsze (ponieważ DummyNum.add(x)i x.add()zasadniczo mają taką samą wydajność).
GZ0,
1
Nawiasem mówiąc, wyraźne list()połączenia są nieco wolniejsze niż rozumienie listy. Dla uczciwego porównania musisz napisać [*map(...)].
GZ0,
@ GZ0 dzięki za świetne opinie! Wszystko to ma sens i nie byłem świadomy, że 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.
craymichael
0

Uważam, że najbardziej pythonicznym sposobem jest użycie rozumienia listy zamiast mapi filter. Powodem jest to, że listy są bardziej zrozumiałe niż mapi filter.

In [1]: odd_cubes = [x ** 3 for x in range(10) if x % 2 == 1] # using a list comprehension

In [2]: odd_cubes_alt = list(map(lambda x: x ** 3, filter(lambda x: x % 2 == 1, range(10)))) # using map and filter

In [3]: odd_cubes == odd_cubes_alt
Out[3]: True

Jak widzisz, zrozumienie nie wymaga dodatkowych lambdawyrażeń w razie mappotrzeby. Co więcej, zrozumienie pozwala również na łatwe filtrowanie, podczas gdy mapwymaga filterzezwolenia na filtrowanie.

lmiguelvargasf
źródło
0

Próbowałem kodu przez @ alex-martelli, ale znalazłem pewne rozbieżności

python -mtimeit -s "xs=range(123456)" "map(hex, xs)"
1000000 loops, best of 5: 218 nsec per loop
python -mtimeit -s "xs=range(123456)" "[hex(x) for x in xs]"
10 loops, best of 5: 19.4 msec per loop

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.

Mohit Raj
źródło
3
To bardzo stare pytanie, a odpowiedź, do której się odnosisz, najprawdopodobniej została napisana w odniesieniu do Pythona 2, gdzie mapzwraca listę. W Pythonie 3 mapjest leniwie oceniany, więc po prostu wywołanie mapnie oblicza żadnego z nowych elementów listy, dlatego otrzymujesz tak krótkie czasy.
kaya3
Myślę, że używasz Python 3.x Kiedy zadałem to pytanie, Python 3 został niedawno wydany, a Python 2.x był bardzo standardowy.
TimothyAWiseman