rozumienie listy vs. lambda + filtr

857

Zdarzyło mi się, że mam podstawową potrzebę filtrowania: mam listę i muszę ją filtrować według atrybutu elementów.

Mój kod wyglądał tak:

my_list = [x for x in my_list if x.attribute == value]

Ale potem pomyślałem, czy nie byłoby lepiej tak napisać?

my_list = filter(lambda x: x.attribute == value, my_list)

Jest bardziej czytelny, a w razie potrzeby można wyjąć lambda, aby coś zyskać.

Pytanie brzmi: czy są jakieś zastrzeżenia dotyczące korzystania z drugiego sposobu? Jakaś różnica w wydajności? Czy całkowicie brakuje mi Pythonic Way ™ i czy powinienem to zrobić w jeszcze inny sposób (np. Używając itemgetter zamiast lambda)?

Agos
źródło
19
Lepszym przykładem może być przypadek, w którym masz już ładnie nazwaną funkcję do wykorzystania jako predykat. W takim przypadku myślę, że o wiele więcej osób zgodziłoby się, że filterbyło to bardziej czytelne. Kiedy masz proste wyrażenie, które mogą być używane jak jest w listcomp, ale musi być zapakowane w lambda (lub podobnie zbudowane z partiallub operatorfunkcji, etc.), aby przekazać filter, że właśnie wtedy listcomps wygrać.
abarnert
3
Należy powiedzieć, że przynajmniej w Pythonie 3 zwracany filterjest obiekt generatora filtrów, a nie lista.
Matteo Ferla,

Odpowiedzi:

588

Dziwne, jak różne jest piękno u różnych ludzi. Uważam, że lista jest znacznie bardziej zrozumiała niż filter+ lambda, ale używaj tego, co uznasz za łatwiejsze.

Są dwie rzeczy, które mogą spowolnić twoje użycie filter.

Pierwszym z nich jest narzut wywołania funkcji: jak tylko użyjesz funkcji Python (utworzonej przez deflub lambda), prawdopodobnie filtr będzie wolniejszy niż przetwarzanie listy. Prawie na pewno nie wystarczy, aby mieć znaczenie, i nie powinieneś dużo myśleć o wydajności, dopóki nie zorientujesz się w swoim kodzie i nie okaże się, że to wąskie gardło, ale będzie różnica.

Innym narzutem, który może mieć zastosowanie, jest to, że lambda jest zmuszona uzyskać dostęp do zmiennej o zasięgu ( value). Jest to wolniejsze niż uzyskiwanie dostępu do zmiennej lokalnej, a w Pythonie 2.x analiza listy ma dostęp tylko do zmiennych lokalnych. Jeśli używasz języka Python 3.x, lista jest uruchamiana w osobnej funkcji, więc dostęp do niej będzie również uzyskiwany valuepoprzez zamknięcie i ta różnica nie będzie miała zastosowania.

Inną opcją do rozważenia jest użycie generatora zamiast zrozumienia listy:

def filterbyvalue(seq, value):
   for el in seq:
       if el.attribute==value: yield el

Następnie w swoim głównym kodzie (gdzie liczy się czytelność) zastąpiłeś zarówno rozumienie listy, jak i filtr, miejmy nadzieję, znaczącą nazwę funkcji.

Duncan
źródło
68
+1 dla generatora. Mam w domu link do prezentacji, która pokazuje, jak niesamowite mogą być generatory. Możesz także zastąpić rozumienie listy wyrażeniem generatora, zmieniając []na (). Zgadzam się również, że kompilacja listy jest piękniejsza.
Wayne Werner
1
W rzeczywistości brak filtrowania jest szybszy. Po prostu uruchom kilka szybkich testów porównawczych, używając czegoś takiego jak stackoverflow.com/questions/5998245/…
skqr
2
@skqr lepiej po prostu użyć timeit do testów porównawczych, ale proszę podać przykład, w którym okazało filtersię, że jesteś szybszy przy użyciu funkcji zwrotnej Python.
Duncan,
8
@ tnq177 To prezentacja Davida Beasleya na temat generatorów - dabeaz.com/generators
Wayne Werner
2
@ VictorSchröder tak, być może byłem niejasny. Próbowałem powiedzieć, że w głównym kodzie musisz widzieć większy obraz. W funkcji małego pomocnika musisz tylko dbać o tę jedną funkcję, a to, co dzieje się na zewnątrz, można zignorować.
Duncan
237

Jest to dość religijna kwestia w Pythonie. Mimo że Guido rozważał usunięcie map, filteraw reducePythonie 3 było dość luzu, który ostatecznie reducezostał przeniesiony tylko z wbudowanych do funools.reduce .

Osobiście uważam, że listy są łatwiejsze do odczytania. Bardziej wyraźne jest to, co dzieje się z wyrażeniem, [i for i in list if i.attribute == value]ponieważ całe zachowanie jest na powierzchni, a nie wewnątrz funkcji filtra.

Nie martwiłbym się zbytnio różnicą wydajności między tymi dwoma podejściami, ponieważ jest ona marginalna. Naprawdę zoptymalizowałbym to tylko, gdyby okazało się, że jest to wąskie gardło w twojej aplikacji, co jest mało prawdopodobne.

Również dlatego, że BDFL chciał filterodejść od języka, to z pewnością automatycznie czyni listy ze zrozumiałymi pythonami ;-)

Tendayi Mawushe
źródło
1
Dzięki za linki do wkładu Guido, jeśli nic więcej dla mnie nie oznacza, że ​​postaram się ich nie używać, aby nie nabrać nawyku i nie
poprzeć
1
ale redukcja jest najbardziej złożona w przypadku prostych narzędzi! Mapa i filtr są trywialne, aby zastąpić je zrozumieniem!
njzk2
8
nie wiedziałem, że redukcja została obniżona w Python3. dzięki za wgląd! redukcja () jest nadal bardzo pomocna w obliczeniach rozproszonych, takich jak PySpark. Myślę, że to był błąd ...
Tagar
1
@Tagar, którego możesz nadal używać, musisz tylko zaimportować z funools
icc97
69

Ponieważ jakakolwiek różnica prędkości musi być niewielka, to czy użyć filtrów, czy też list, sprowadza się to do gustu. Ogólnie rzecz biorąc, jestem skłonny używać rozumienia (co wydaje się zgadzać z większością innych odpowiedzi tutaj), ale jest jeden przypadek, w którym wolę filter.

Bardzo częstym przypadkiem użycia jest wyciągnięcie wartości jakiegoś iterowalnego X z zastrzeżeniem predykatu P (x):

[x for x in X if P(x)]

ale czasami chcesz najpierw zastosować jakąś funkcję do wartości:

[f(x) for x in X if P(f(x))]


Jako konkretny przykład rozważmy

primes_cubed = [x*x*x for x in range(1000) if prime(x)]

Myślę, że wygląda to nieco lepiej niż używanie filter. Ale teraz zastanów się

prime_cubes = [x*x*x for x in range(1000) if prime(x*x*x)]

W tym przypadku chcemy filterprzeciw obliczonej wartości obliczonej. Oprócz kwestii dwukrotnego obliczenia kostki (wyobraź sobie droższe obliczenia), istnieje także kwestia dwukrotnego napisania wyrażenia, co narusza estetykę SUCHEGO . W takim przypadku byłbym skłonny użyć

prime_cubes = filter(prime, [x*x*x for x in range(1000)])
IJ Kennedy
źródło
7
Czy nie rozważyłbyś użycia liczby pierwszej za pomocą innego zrozumienia listy? Takich jak[prime(i) for i in [x**3 for x in range(1000)]]
viki.omega9,
20
x*x*xnie może być liczbą pierwszą, ponieważ tak jest x^2i xtak naprawdę, przykład ten nie ma sensu w sensie matematycznym, ale może nadal jest pomocny. (Może moglibyśmy znaleźć coś lepszego?)
Zelphir Kaltstahl
3
Zauważ, że zamiast ostatniego przykładu możemy użyć wyrażenia generatora, jeśli nie chcemy zużywać pamięci:prime_cubes = filter(prime, (x*x*x for x in range(1000)))
Mateen Ulhaq
4
@MateenUlhaq można to zoptymalizować, aby prime_cubes = [1]zaoszczędzić zarówno pamięć, jak i cykle procesora ;-)
Dennis Krupenik
7
@DennisKrupenik A raczej[]
Mateen Ulhaq
29

Chociaż filtermoże to być „szybszy sposób”, „pytonicznym sposobem” byłoby nie przejmowanie się takimi rzeczami, chyba że wydajność jest absolutnie krytyczna (w takim przypadku nie używałbyś Pythona!).

Umang
źródło
9
Późny komentarz do często spotykanego argumentu: Czasami istotna jest analiza przeprowadzana w ciągu 5 godzin zamiast 10, a jeśli można to osiągnąć, zajmując godzinę na optymalizację kodu python, może być tego warte (szczególnie jeśli wygodne w Pythonie, a nie w szybszych językach).
bli
Ale ważniejsze jest to, jak bardzo kod źródłowy spowalnia nas, próbując go przeczytać i zrozumieć!
thoni56
20

Pomyślałem, że po prostu dodam, że w Pythonie 3 filter () jest w rzeczywistości obiektem iteratora, więc musisz zbudować filtrowaną listę do wywołania metody filter do list (). Tak więc w python 2:

lst_a = range(25) #arbitrary list
lst_b = [num for num in lst_a if num % 2 == 0]
lst_c = filter(lambda num: num % 2 == 0, lst_a)

listy b i c mają te same wartości i zostały uzupełnione mniej więcej w tym samym czasie, gdy filter () był równoważny [x dla x in y, jeśli z]. Jednak w 3 ten sam kod pozostawiłby listę c zawierającą obiekt filtrujący, a nie filtrowaną listę. Aby wygenerować te same wartości w 3:

lst_a = range(25) #arbitrary list
lst_b = [num for num in lst_a if num % 2 == 0]
lst_c = list(filter(lambda num: num %2 == 0, lst_a))

Problem polega na tym, że list () przyjmuje iterowalny argument i tworzy nową listę z tego argumentu. Powoduje to, że użycie filtru w ten sposób w Pythonie 3 zajmuje nawet dwukrotnie więcej czasu niż metoda [x for x in y if z], ponieważ musisz iterować dane wyjściowe z filter (), a także oryginalną listę.

Jim50
źródło
13

Istotną różnicą jest to, że zrozumienie listy zwróci listchwilę, podczas gdy filtr zwróci a filter, którego nie można manipulować jak list(tj. Wywołanie lengo, które nie działa z powrotemfilter ).

Mój własny proces uczenia się doprowadził mnie do podobnego problemu.

Biorąc to pod uwagę, jeśli istnieje sposób, aby uzyskać wynik listz filter, trochę tak jak zrobiłbyś w .NET, kiedy to zrobiszlst.Where(i => i.something()).ToList() , jestem ciekawy, aby to wiedzieć.

EDYCJA: Tak jest w przypadku Pythona 3, a nie 2 (patrz dyskusja w komentarzach).

Adeynack
źródło
4
filtr zwraca listę i możemy na niej użyć len. Przynajmniej w moim Pythonie 2.7.6.
thiruvenkadam
7
W Pythonie 3 tak nie jest a = [1, 2, 3, 4, 5, 6, 7, 8] f = filter(lambda x: x % 2 == 0, a) lc = [i for i in a if i % 2 == 0] >>> type(f) <class 'filter'> >>> type(lc) <class 'list'>
Adeynack
3
„jeśli istnieje sposób na uzyskanie wynikowej listy ... Jestem ciekawy, aby ją poznać”. Wystarczy zadzwonić list()na wynik: list(filter(my_func, my_iterable)). I oczywiście można zastąpić listz set, lub tuple, lub cokolwiek innego, która pobiera iterable. Ale dla kogokolwiek innego niż programistów funkcjonalnych, sprawa jest jeszcze silniejsza, aby użyć zrozumienia listy, a nie filterwyraźnej konwersji na list.
Steve Jessop
10

Drugi sposób uważam za bardziej czytelny. Mówi dokładnie, jaki jest zamiar: przefiltruj listę.
PS: nie używaj „listy” jako nazwy zmiennej

unbeli
źródło
7

ogólnie filterjest nieco szybszy, jeśli używasz wbudowanej funkcji.

Spodziewałbym się, że lista będzie nieco szybsza w twoim przypadku

John La Rooy
źródło
python -m timeit 'filter (lambda x: x in [1,2,3,4,5], range (10000000))' 10 pętli, najlepiej 3: 1,44 s na pętlę python -m timeit '[x dla x in range (10000000) if x in [1,2,3,4,5]] 10 pętli, najlepiej 3: 860 ms na pętlę Naprawdę ?!
giaosudau
@sepdau, funkcje lambda nie są wbudowane. Rozumienie listy uległo poprawie w ciągu ostatnich 4 lat - teraz i tak różnica jest znikoma, nawet przy wbudowanych funkcjach
John La Rooy
7

Filtr jest właśnie taki. Filtruje elementy listy. Możesz zobaczyć, że definicja wspomina to samo (w oficjalnym linku do dokumentów, o którym wspomniałem wcześniej). Podczas gdy rozumienie listy jest czymś, co tworzy nową listę po działaniu na coś z poprzedniej listy (zarówno filtrowanie, jak i rozumienie listy tworzy nową listę i nie wykonuje operacji zamiast starszej listy. Nowa lista tutaj jest czymś w rodzaju listy z , powiedzmy, zupełnie nowy typ danych. Jak konwersja liczb całkowitych na ciąg itp.)

W twoim przykładzie lepiej jest używać filtrowania niż listowania, zgodnie z definicją. Jeśli jednak chcesz, powiedzmy inny atrybut z elementów listy, w twoim przykładzie ma zostać pobrana jako nowa lista, możesz użyć rozumienia listy.

return [item.other_attribute for item in my_list if item.attribute==value]

Tak właśnie pamiętam o analizie filtrów i list. Usuń kilka rzeczy z listy i pozostań nienaruszonych, użyj filtru. Skorzystaj z własnej logiki przy elementach i stwórz rozwodnioną listę odpowiednią do określonego celu, skorzystaj ze zrozumienia listy.

thiruvenkadam
źródło
2
Z przyjemnością poznam powód głosowania w dół, aby nie powtarzać go nigdzie w przyszłości.
thiruvenkadam
definicja filtru i listy nie była konieczna, ponieważ ich znaczenie nie było dyskutowane. To, że rozumienie listy powinno być używane tylko w przypadku „nowych” list, jest przedstawione, ale nie jest uzasadnione.
Agos
Użyłem definicji, aby powiedzieć, że filtr daje listę z tymi samymi elementami, które są prawdziwe dla przypadku, ale dzięki zrozumieniu listy możemy modyfikować same elementy, na przykład konwersję int na str. Ale punkt wzięty :-)
thiruvenkadam
4

Oto krótki fragment, którego używam, gdy muszę odfiltrować coś po z listą. Po prostu kombinacja filtra, lambda i list (zwana także lojalnością kota i czystością psa).

W tym przypadku czytam plik, usuwam puste linie, komentuję linie i wszystko po komentarzu do linii:

# Throw out blank lines and comments
with open('file.txt', 'r') as lines:        
    # From the inside out:
    #    [s.partition('#')[0].strip() for s in lines]... Throws out comments
    #   filter(lambda x: x!= '', [s.part... Filters out blank lines
    #  y for y in filter... Converts filter object to list
    file_contents = [y for y in filter(lambda x: x != '', [s.partition('#')[0].strip() for s in lines])]
rharder
źródło
Osiąga to bardzo dużo w bardzo małym kodzie. Myślę, że może to być trochę za dużo logiki w jednej linii, aby łatwo ją zrozumieć, ale liczy się czytelność.
Zelphir Kaltstahl
Możesz to napisać jakofile_contents = list(filter(None, (s.partition('#')[0].strip() for s in lines)))
Steve Jessop
4

Oprócz zaakceptowanej odpowiedzi istnieje przypadek narożny, w którym należy użyć filtru zamiast listy. Jeśli listy nie da się zhashować, nie można jej przetworzyć bezpośrednio ze zrozumieniem listy. Rzeczywistym przykładem jest sytuacja, gdy używasz pyodbcdo odczytu wyników z bazy danych. Te fetchAll()wyniki cursorto lista unhashable. W tej sytuacji, aby bezpośrednio manipulować zwróconymi wynikami, należy użyć filtru:

cursor.execute("SELECT * FROM TABLE1;")
data_from_db = cursor.fetchall()
processed_data = filter(lambda s: 'abc' in s.field1 or s.StartTime >= start_date_time, data_from_db) 

Jeśli użyjesz tutaj rozumienia listy, pojawi się błąd:

TypeError: unhashable type: 'list'

CWpraen
źródło
1
wszystkie listy są bezwzględne, >>> hash(list()) # TypeError: unhashable type: 'list'po drugie, to działa dobrze:processed_data = [s for s in data_from_db if 'abc' in s.field1 or s.StartTime >= start_date_time]
Thomas Grainger
„Jeśli listy nie da się zhashować, nie można jej bezpośrednio przetworzyć przy użyciu listy.” To nie jest prawda, a i tak wszystkie listy są nie do powstrzymania.
juanpa.arrivillaga
3

Zajęło mi trochę czasu, aby zapoznać się z higher order functions filteri map. Więc przyzwyczaiłem się do nich i naprawdę mi się podobało, filterponieważ było jasne, że filtruje, zachowując wszystko, co jest prawdą, i czułem się fajnie, że znałem niektóre functional programmingwarunki.

Potem czytam ten fragment (Fluent Python Book):

Funkcje mapowania i filtrowania są nadal wbudowane w Pythona 3, ale od czasu wprowadzenia list i wyrażeń generatora nie są one tak ważne. Listcomp lub genexp wykonują zadanie mapowania i filtrowania łącznie, ale są bardziej czytelne.

A teraz myślę, po co zawracać sobie głowę koncepcją filter/ mapjeśli możesz to osiągnąć dzięki już szeroko rozpowszechnionym idiomom, takim jak listy. Ponadto mapsi filterssą rodzajem funkcji. W tym przypadku wolę używać Anonymous functionslambda.

Wreszcie, tylko ze względu na przetestowanie, określiłem obie metody ( mapi listComp) i nie zauważyłem żadnej istotnej różnicy prędkości, która uzasadniałaby argumentowanie na ten temat.

from timeit import Timer

timeMap = Timer(lambda: list(map(lambda x: x*x, range(10**7))))
print(timeMap.timeit(number=100))

timeListComp = Timer(lambda:[(lambda x: x*x) for x in range(10**7)])
print(timeListComp.timeit(number=100))

#Map:                 166.95695265199174
#List Comprehension   177.97208347299602
użytkownik1767754
źródło
0

Co ciekawe, w Pythonie 3 widzę, że filtr działa szybciej niż wyrażenia listowe.

Zawsze myślałem, że listy będą bardziej wydajne. Coś w stylu: [nazwa dla nazwy w brand_names_db, jeśli nazwa nie jest Brak] Wygenerowany kod bajtowy jest nieco lepszy.

>>> def f1(seq):
...     return list(filter(None, seq))
>>> def f2(seq):
...     return [i for i in seq if i is not None]
>>> disassemble(f1.__code__)
2         0 LOAD_GLOBAL              0 (list)
          2 LOAD_GLOBAL              1 (filter)
          4 LOAD_CONST               0 (None)
          6 LOAD_FAST                0 (seq)
          8 CALL_FUNCTION            2
         10 CALL_FUNCTION            1
         12 RETURN_VALUE
>>> disassemble(f2.__code__)
2           0 LOAD_CONST               1 (<code object <listcomp> at 0x10cfcaa50, file "<stdin>", line 2>)
          2 LOAD_CONST               2 ('f2.<locals>.<listcomp>')
          4 MAKE_FUNCTION            0
          6 LOAD_FAST                0 (seq)
          8 GET_ITER
         10 CALL_FUNCTION            1
         12 RETURN_VALUE

Ale w rzeczywistości są wolniejsze:

   >>> timeit(stmt="f1(range(1000))", setup="from __main__ import f1,f2")
   21.177661532000116
   >>> timeit(stmt="f2(range(1000))", setup="from __main__ import f1,f2")
   42.233950221000214
Rod Senra
źródło
8
Niepoprawne porównanie . Po pierwsze, nie przekazujesz funkcji lambda do wersji filtru, co czyni ją domyślną funkcją tożsamości. Definiując if not Nonew listowego ty zdefiniowaniu funkcji lambda (zawiadomienie MAKE_FUNCTIONoświadczenie). Po drugie, wyniki są różne, ponieważ wersja ze zrozumieniem listy usunie tylko Nonewartość, natomiast wersja filtru usunie wszystkie wartości „fałszowania”. Powiedziawszy to, cały cel znakowania mikrobiologicznego jest bezużyteczny. To milion powtórzeń, razy 1k przedmiotów! Różnica jest znikoma .
Victor Schröder
-7

Moje podanie

def filter_list(list, key, value, limit=None):
    return [i for i in list if i[key] == value][:limit]
tim
źródło
3
inigdy nie było powiedziane dict, i nie ma takiej potrzeby limit. Poza tym, czym to się różni od tego, co sugeruje PO i jak odpowiada na pytanie?