Rozumienie listy bez [] w Pythonie

85

Dołączanie do listy:

>>> ''.join([ str(_) for _ in xrange(10) ])
'0123456789'

join musi mieć iterowalny plik.

Najwyraźniej joinargumentem jest [ str(_) for _ in xrange(10) ]i jest to zrozumienie listy .

Spójrz na to:

>>>''.join( str(_) for _ in xrange(10) )
'0123456789'

Otóż joinargumentem jest po prostu str(_) for _ in xrange(10)nie [], ale wynik jest taki sam.

Czemu? Czy str(_) for _ in xrange(10)tworzy również listę lub iterowalną listę?

Alcott
źródło
1
Wyobrażam sobie, że joinnajprawdopodobniej jest napisane w C i dlatego działa znacznie szybciej niż zrozumienie listy ... Czas testu!
Joel Cornett
Najwyraźniej całkowicie źle odczytałem twoje pytanie. Wygląda na to, że zwraca mi generator ...
Joel Cornett
18
Tylko uwaga: _nie ma specjalnego znaczenia, jest to zwykła nazwa zmiennej. Często jest używana jako nazwa do wyrzucenia, ale tak nie jest (używasz zmiennej). Unikałbym używania go w kodzie (przynajmniej w ten sposób).
rplnt

Odpowiedzi:

67
>>>''.join( str(_) for _ in xrange(10) )

Nazywa się to wyrażeniem generatora i zostało wyjaśnione w PEP 289 .

Główna różnica między wyrażeniami generatora a wyrażeniami list polega na tym, że te pierwsze nie tworzą listy w pamięci.

Zwróć uwagę, że istnieje trzeci sposób zapisania wyrażenia:

''.join(map(str, xrange(10)))
NPE
źródło
1
Z tego co wiem, generator można utworzyć za pomocą wyrażenia przypominającego krotkę, na przykład ( str(_) for _ in xrange(10) ). Ale byłem zdezorientowany, dlaczego ()można pominąć w join, co oznacza, że ​​kod powinien wyglądać jak `` '' .join ((str (_) for _ in xrange (10))), prawda?
Alcott,
1
@Alcott Moje rozumienie krotek jest takie, że w rzeczywistości są one definiowane przez listę wyrażeń oddzielonych przecinkami, a nie przez nawiasy; nawiasy są tam tylko po to, aby wizualnie pogrupować wartości w przypisaniu lub faktycznie pogrupować wartości, jeśli krotka przechodzi do innej listy oddzielonej przecinkami, jak wywołanie funkcji. Jest to często demonstrowane przez uruchomienie kodu, takiego jak tup = 1, 2, 3; print(tup). Mając to na uwadze, użycie forjako części wyrażenia tworzy generator, a nawiasy służą tylko do odróżnienia go od nieprawidłowo napisanej pętli.
Eric Ed Lohmar
132

Pozostali respondenci mieli rację, odpowiadając, że znalazłeś wyrażenie generatora (które ma notację podobną do wyrażeń listowych, ale bez otaczających nawiasów kwadratowych).

Ogólnie rzecz biorąc, geneksy (jak są czule znane) są bardziej wydajne w pamięci i szybsze niż rozumienia list.

JEDNAK w przypadku ''.join(), rozumienie listy jest zarówno szybsze, jak i bardziej wydajne w pamięci. Powodem jest to, że złączenie musi wykonać dwa przejścia przez dane, więc w rzeczywistości potrzebuje prawdziwej listy. Jeśli go podasz, może natychmiast rozpocząć pracę. Jeśli zamiast tego podasz mu genexp, nie będzie mógł rozpocząć pracy, dopóki nie utworzy nowej listy w pamięci, uruchamiając genexp do wyczerpania:

~ $ python -m timeit '"".join(str(n) for n in xrange(1000))'
1000 loops, best of 3: 335 usec per loop
~ $ python -m timeit '"".join([str(n) for n in xrange(1000)])'
1000 loops, best of 3: 288 usec per loop

Ten sam wynik zachodzi przy porównaniu itertools.imap z mapą :

~ $ python -m timeit -s'from itertools import imap' '"".join(imap(str, xrange(1000)))'
1000 loops, best of 3: 220 usec per loop
~ $ python -m timeit '"".join(map(str, xrange(1000)))'
1000 loops, best of 3: 212 usec per loop
Raymond Hettinger
źródło
4
@lazyr Twój drugi czas wykonuje zbyt dużo pracy. Nie zawijaj genexp wokół listcomp - po prostu użyj bezpośrednio genexp. Nic dziwnego, że masz dziwne czasy.
Raymond Hettinger
11
Czy możesz wyjaśnić, dlaczego ''.join()do zbudowania łańcucha potrzebne są 2 przejścia przez iterator?
ovgolovin
27
@ovgolovin Wydaje mi się, że pierwszym przebiegiem jest zsumowanie długości ciągów, aby móc przydzielić odpowiednią ilość pamięci dla połączonego ciągu, podczas gdy drugi przebieg polega na skopiowaniu poszczególnych ciągów do przydzielonej przestrzeni.
Lauritz V. Thaulow
20
@lazyr To przypuszczenie jest poprawne. Dokładnie to robi str.join :-)
Raymond Hettinger
4
Czasami naprawdę brakuje mi możliwości „ulubionych” konkretnej odpowiedzi w SO.
Air
5

Twój drugi przykład używa raczej wyrażenia generatora niż rozumienia listy. Różnica polega na tym, że przy zrozumieniu list lista jest całkowicie budowana i przekazywana do .join(). Dzięki wyrażeniu generatora elementy są generowane jeden po drugim i używane przez .join(). Ten ostatni zużywa mniej pamięci i jest generalnie szybszy.

Tak się składa, że ​​konstruktor listy z radością wykorzysta każdą iterację, w tym wyrażenie generatora. Więc:

[str(n) for n in xrange(10)]

to tylko „cukier syntaktyczny” dla:

list(str(n) for n in xrange(10))

Innymi słowy, rozumienie listy jest po prostu wyrażeniem generatora, które jest przekształcane w listę.

kindall
źródło
2
Czy na pewno są równoważne pod maską? Timeit mówi:: [str(x) for x in xrange(1000)]262 usec,: list(str(x) for x in xrange(1000))304 usec.
Lauritz V. Thaulow
2
@lazyr Masz rację. Zrozumienie listy jest szybsze. I to jest powód, dla którego listy składane wyciekają w Pythonie 2.x. Oto, co napisał GVR: "" To był artefakt oryginalnej implementacji list złożonych; był to jeden z „brudnych małych sekretów” Pythona przez lata. Zaczęło się od celowego kompromisu mającego na celu oślepiająco szybkie tworzenie list i chociaż nie było to częstą pułapką dla początkujących, to z pewnością od czasu do czasu kłuło
ovgolovin
3
@ovgolovin Powodem, dla którego listcomp jest szybszy, jest to, że join musi utworzyć listę, zanim będzie mogła rozpocząć pracę. „Wyciek”, do którego się odnosisz, nie jest problemem z szybkością - oznacza po prostu, że zmienna indukcyjna pętli jest ujawniona poza listcomp.
Raymond Hettinger
1
@RaymondHettinger W takim razie co oznaczają te słowa „Zaczęło się od celowego kompromisu, aby oślepiająco szybko tworzyć listy ze składaniem ”? Jak zrozumiałem, istnieje związek między ich wyciekiem a problemami z szybkością. GVR napisał również: "W przypadku wyrażeń generatora nie mogliśmy tego zrobić. Wyrażenia generatora są implementowane przy użyciu generatorów, których wykonanie wymaga oddzielnej ramki wykonania. Dlatego wyrażenia generatora (zwłaszcza jeśli iterują po krótkiej sekwencji) były mniej wydajne niż wyrażenia listowe . "
ovgolovin
4
@ovgolovin Wykonałeś nieprawidłowy skok od szczegółów implementacji listcomp do tego, dlaczego str.join działa tak, jak to robi. Jedna z pierwszych linii w kodzie str.join to seq = PySequence_Fast(orig, "");i jest to jedyny powód, dla którego iteratory działają wolniej niż listy lub krotki podczas wywoływania str.join (). Zapraszamy do rozpoczęcia czatu, jeśli chcesz o nim dalej dyskutować (jestem autorem PEP 289, twórcą kodu operacji LIST_APPEND i tym, który zoptymalizował konstruktor list (), więc mam trochę znajomość zagadnienia).
Raymond Hettinger
5

Jak wspomniano, jest to wyrażenie generatora .

Z dokumentacji:

Nawiasy można pominąć w przypadku wywołań z tylko jednym argumentem. Szczegółowe informacje można znaleźć w sekcji Zaproszenia .

monkut
źródło
4

Jeśli jest w parens, ale nie w nawiasach, jest technicznie wyrażeniem generatora. Wyrażenia generatora zostały po raz pierwszy wprowadzone w Pythonie 2.4.

http://wiki.python.org/moin/Generators

Część po złączeniu ( str(_) for _ in xrange(10) )jest sama w sobie wyrażeniem generującym. Możesz zrobić coś takiego:

mylist = (str(_) for _ in xrange(10))
''.join(mylist)

i oznacza to dokładnie to samo, co napisałeś w drugim przypadku powyżej.

Generatory mają kilka bardzo interesujących właściwości, z których najważniejszą jest to, że nie przydzielają całej listy, gdy jej nie potrzebujesz. Zamiast tego funkcja taka jak join „wypompowuje” elementy z wyrażenia generatora pojedynczo, wykonując swoją pracę na malutkich częściach pośrednich.

W twoich konkretnych przykładach lista i generator prawdopodobnie nie działają strasznie inaczej, ale generalnie wolę używać wyrażeń generatora (a nawet funkcji generatora), kiedy tylko mogę, głównie dlatego, że niezwykle rzadko generator działa wolniej niż pełna lista materializacja.

sblom
źródło
1

To jest raczej generator niż rozumienie listy. Generatory są również iterowalne, ale zamiast najpierw tworzyć całą listę, a następnie przekazywać ją do złączenia, przekazuje każdą wartość z zakresu x po kolei, co może być znacznie bardziej wydajne.

Daniel Roseman
źródło
0

Argumentem twojego drugiego joinwywołania jest wyrażenie generatora. Tworzy iterowalny plik.

Michael J. Barber
źródło