розуміння списку та лямбда + фільтр


857

У мене трапилося, що у мене є основна потреба у фільтрації: у мене є список, і я мушу відфільтрувати його за атрибутом елементів.

Мій код виглядав так:

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

Але тоді я подумав, чи не було б краще написати так?

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

Це читабельніше, і якщо це потрібно для виконання, лямбду можна вивезти, щоб щось набрати.

Питання: чи є якісь застереження щодо використання другого способу? Будь-яка різниця у виконанні? Чи повністю я пропускаю Pythonic Way ™ і чи варто це робити ще одним способом (наприклад, використовуючи itemgetter замість лямбда)?


19
Кращим прикладом може бути випадок, коли у вас уже була чітко названа функція, яку слід використовувати як свій предикат. У цьому випадку я думаю, що набагато більше людей погодиться, що це filterбуло легше читати. Коли у вас є простий вираз, який можна використовувати як-є у listcomp, але його потрібно загорнути в лямбда (або аналогічно побудовано з функцій partialабо operatorфункцій тощо), щоб перейти до filterцього, саме тоді listcomps виграє.
abarnert

3
Слід сказати, що принаймні в Python3 повернення filter- це об'єкт генератора фільтрів, а не список.
Маттео Ферла

Відповіді:


588

Дивно, наскільки змінюється краса у різних людей. Я вважаю, що розуміння списку набагато чіткіше, ніж filter+ lambda, але використовувати те, що вам легше.

Є дві речі, які можуть уповільнити ваше використання filter.

Перший - накладний виклик функції: як тільки ви використовуєте функцію Python (створена defабо lambda), цілком ймовірно, що фільтр буде повільніше, ніж розуміння списку. Це майже напевно недостатньо для того, щоб мати значення, і вам не варто думати над ефективністю, поки ви не приурочили код і не виявили, що це вузьке місце, але різниця буде там.

Інший наклад, який може застосуватись, полягає в тому, що лямбда змушена отримувати доступ до масштабної змінної ( value). Це повільніше, ніж доступ до локальної змінної і в Python 2.x розуміння списку має доступ лише до локальних змінних. Якщо ви використовуєте Python 3.x, розуміння списку працює в окремій функції, тому він також матиме доступ valueчерез закриття, і ця різниця не застосовуватиметься.

Інший варіант, який слід розглянути, - це використовувати генератор замість розуміння списку:

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

Тоді у своєму головному коді (саме там, де читабельність насправді має значення) ви замінили розуміння списку та фільтрували з надією значущою назвою функції.


68
+1 для генератора. У мене вдома є посилання на презентацію, яка показує, якими дивовижними можуть бути генератори. Ви також можете замінити список розуміння з виразом генератора тільки за рахунок зміни []в (). Також я погоджуюся, що список комп є красивіший.
Уейн Вернер

1
На насправді, немає - фільтр не швидше. Просто запустіть кілька швидких тестів , використовуючи що - щось на зразок stackoverflow.com/questions/5998245 / ...
skqr

2
@skqr краще просто використовувати timeit для орієнтирів, але, будь ласка, наведіть приклад, коли ви filterшвидше використовуєте функцію зворотного виклику Python.
Дункан

8
@ tnq177 Це презентація Девіда Біслі про генератори - dabeaz.com/generators
Уейн Вернер

2
@ VictorSchröder так, можливо, мені було незрозуміло. Що я намагався сказати, це те, що в основному коді потрібно вміти бачити більшу картину. У маленькій помічниковій функції вам потрібно дбати лише про цю одну функцію, те, що ще відбувається назовні, можна ігнорувати.
Дункан

237

Це дещо релігійне питання в Python. Навіть незважаючи на те, що Guido розглядав питання про видалення map, filterі reduceз Python 3 , було достатньо зворотного зв'язку, який врешті-решт reduceбув переміщений із вбудованих модулів у functools.reduce .

Особисто мені здається, що ознайомлення зі списком легше читати. Це виразніше, що відбувається з виразом, [i for i in list if i.attribute == value]оскільки вся поведінка знаходиться на поверхні, яка не знаходиться у функції фільтра.

Я б не переймався надто різницею в продуктивності між двома підходами, оскільки це незначно. Я б дійсно оптимізував це лише в тому випадку, якщо він виявиться вузьким місцем у вашій заявці, що малоймовірно.

Крім того, оскільки BDFL хотів filterпіти з мови, то, безумовно, це автоматично робить розуміння списку більш пітонічним ;-)


1
Дякую за посилання на вклад Гвідо, якщо нічого іншого для мене це не означає, що я намагатимусь більше не використовувати їх, щоб не здобути звичку, і не стану прихильником цієї релігії :)
dashesy

1
але зменшити - це найскладніше зробити з простих інструментів! карта та фільтр є тривіальними для заміни розуміннями!
njzk2

8
не знав зниження було знижено в Python3. дякую за розуміння! redu () все ще дуже допомагає в розподілених обчисленнях, як-от PySpark. Я думаю, що це була помилка ..
Tagar

1
@Tagar ви все ще можете зменшити, вам просто доведеться імпортувати його з functools
icc97

69

Оскільки будь-яка різниця швидкостей має бути незначною, то, чи використовувати фільтри чи переліки списку, зводиться до смаку. Взагалі я схильний використовувати розуміння (що, здається, погоджується з більшістю інших відповідей тут), але є один випадок, коли я віддаю перевагу filter.

Дуже частим випадком використання є витягнення значень деяких ітерабельних X, що підлягають предикату P (x):

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

але іноді потрібно спочатку застосувати якусь функцію до значень:

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


В якості конкретного прикладу розглянемо

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

Я думаю, це виглядає трохи краще, ніж використання filter. Але тепер розглянемо

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

У цьому випадку ми хочемо filterпроти значення після обчислення. Окрім питання обчислення куба удвічі (уявіть собі дорожчий розрахунок), існує питання написання виразу двічі, що порушує естетику DRY . У цьому випадку я був би в нагоді використовувати

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

7
Ви б не розглядали можливість використання праймера через інше розуміння списку? Такі як[prime(i) for i in [x**3 for x in range(1000)]]
viki.omega9

20
x*x*xне може бути простим числом, як це є, x^2і xяк фактор, приклад насправді не має сенсу математично, але, можливо, це все-таки helpul. (Може, ми могли б знайти щось краще?)
Зельфір Кальтшталь

3
Зауважте, що ми можемо використовувати генераторний вираз замість останнього прикладу, якщо ми не хочемо з'їдати пам'ять:prime_cubes = filter(prime, (x*x*x for x in range(1000)))
Mateen Ulhaq

4
@MateenUlhaq це можна оптимізувати для prime_cubes = [1]збереження циклів пам'яті та процесора ;-)
Dennis Krupenik

7
@DennisKrupenik А точніше,[]
Mateen Ulhaq

29

Хоча це filterможе бути "швидшим способом", "Pythonic шлях" буде не турбуватися про такі речі, якщо продуктивність не є абсолютно критичною (у такому випадку ви б не використовували Python!).


9
Пізній коментар до часто зустрічаного аргументу: Іноді важливо, щоб аналіз був запущений за 5 годин замість 10, і якщо цього можна досягти, взявши одну годину оптимізації коду python, це може бути вартим цього (особливо якщо такий зручні з python, а не зі швидшими мовами).
блі

Але важливіше те, наскільки вихідний код сповільнює нас, намагаючись прочитати та зрозуміти його!
thoni56

20

Я подумав, що я просто додам, що в python 3, filter () - це насправді ітератор, тому вам доведеться передати свій виклик методу фільтру list (), щоб створити відфільтрований список. Так у 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)

списки b і c мають однакові значення і були виконані приблизно в той же час, що filter () було еквівалентним [x для x in y, якщо z]. Однак у 3 цей самий код залишив би список c, що містить об'єкт фільтра, а не відфільтрований список. Для отримання однакових значень у 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))

Проблема полягає в тому, що list () приймає ітерабельний аргумент і створює новий список із цього аргументу. Результат полягає в тому, що використання фільтра таким чином у python 3 займає вдвічі більше часу, ніж метод [x for x in y if z], тому що вам доведеться перебирати вихід з filter (), а також оригінальний список.


13

Важлива відмінність полягає в тому, що розуміння списку повернеться деякий listчас, коли фільтр поверне a filter, яким ви не можете маніпулювати як list(наприклад: дзвінок lenна нього, який не працює з поверненням filter).

Моє власне самонавчання підштовхнуло мене до якогось подібного питання.

Якщо говорити, якщо є спосіб отримати результат listвід filter, трохи, як ви робите в .NET, коли це робите lst.Where(i => i.something()).ToList(), мені цікаво це знати.

EDIT: Це стосується Python 3, а не 2 (див. Обговорення в коментарях).


4
filter повертає список, і ми можемо використовувати len на ньому. Принаймні в моєму Python 2.7.6.
thiruvenkadam

7
Це не так у Python 3. 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
"якщо є спосіб отримати список, що виходить, мені цікаво це знати". Просто зателефонуйте list()на результат: list(filter(my_func, my_iterable)). І, звичайно , ви могли б замінити listз set, або tuple, або що - небудь ще , що потрібно итератор. Але для будь-кого, крім функціональних програмістів, справа ще сильніше використовувати розуміння списку, а не filterплюс явне перетворення на list.
Стів Джессоп

10

Я вважаю другий спосіб читабельнішим. Це точно говорить вам про те, що є наміром: фільтруйте список.
PS: не використовуйте "list" як ім'я змінної


7

як правило filter, трохи швидше, якщо використовується вбудована функція.

Я б очікував, що осмислення списку у вашому випадку буде трохи швидшим


python -m timeit 'фільтр (лямбда x: x в [1,2,3,4,5], діапазон (10000000))' 10 циклів, найкраще 3: 1,44 сек на цикл python -m timeit '[x for x в діапазоні (10000000), якщо х у [1,2,3,4,5]] '10 петель, найкраще 3: 860 мс на цикл Не справді ?!
giaosudau

@sepdau, лямбда-функції не є вбудованими. Ознайомлення зі списком покращилися за останні 4 роки - тепер різниця незначна навіть у вбудованих функціях
Джон Ла Руй

7

Фільтр - це саме те. Він фільтрує елементи списку. Ви можете побачити визначення, яке згадується про те саме (у офіційному посиланні на документи, про яке я згадував раніше). В той час, як розуміння списку - це те, що створює новий список після дії на щось з попереднього списку (І фільтрування, і розуміння списку створює новий список, а не виконує операції замість старого списку. Новий список тут є чимось на зразок списку з , скажімо, абсолютно новий тип даних. Як перетворення цілих чисел у рядок тощо)

У вашому прикладі краще використовувати фільтр, ніж розуміння списку, відповідно до визначення. Однак, якщо ви хочете, скажімо, інші_attribute зі списку елементів, у вашому прикладі потрібно отримати як новий список, тоді ви можете використовувати розуміння списку.

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

Ось як я насправді пам’ятаю про розуміння фільтру та списку. Видаліть декілька речей із списку, а інші елементи збережете недоторканими, використовуйте фільтр. Використовуйте певну логіку на елементах і створіть список, що поливається, підходящий для певних цілей, використовуйте розуміння списку.


2
Я буду радий дізнатися причину відмови від голосування, щоб більше не повторюватися ніде в майбутньому.
thiruvenkadam

визначення поняття фільтра та списку не було необхідним, оскільки їх значення не обговорювалося. Що розуміння списку слід використовувати лише для "нових" списків, подано, але не сперечається.
Agos

Я використовував визначення, щоб сказати, що фільтр надає вам список з тими ж елементами, які відповідають дійсності для випадку, але з розумінням списку ми можемо змінювати самі елементи, як перетворення int в str. Але точка взята :-)
thuuvenkadam

4

Ось короткий фрагмент, який я використовую, коли мені потрібно щось фільтрувати після розуміння списку. Просто поєднання фільтра, лямбда та списків (інакше відомих як лояльність кота та чистота собаки).

У цьому випадку я читаю файл, викреслюю порожні рядки, коментовані рядки та будь-що після коментаря до рядка:

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

Це дійсно досягає дуже багато за дуже мало коду. Я думаю, що це може бути занадто багато логіки в одному рядку, щоб легко зрозуміти і читабельність - це те, що враховується.
Зельфір Кальтшталь

Ви можете написати це якfile_contents = list(filter(None, (s.partition('#')[0].strip() for s in lines)))
Стів Джессоп

4

Окрім прийнятої відповіді, є важливий випадок, коли слід використовувати фільтр замість розуміння списку. Якщо список неможливий, ви не можете безпосередньо обробити його з розумінням списку. Приклад із реального світу - якщо ви pyodbcчитаєте результати з бази даних. У fetchAll()результатах cursorє unhashable списку. У цій ситуації, щоб безпосередньо маніпулювати поверненими результатами, слід використовувати фільтр:

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) 

Якщо ви використаєте розуміння списку тут, ви отримаєте помилку:

TypeError: unhashable type: 'список'


1
всі списки є непорушними, >>> hash(list()) # TypeError: unhashable type: 'list'по-друге, це працює добре:processed_data = [s for s in data_from_db if 'abc' in s.field1 or s.StartTime >= start_date_time]
Томас Грейнджер

"Якщо список неможливо змінити, ви не можете безпосередньо обробити його з розумінням списку." Це неправда, і всі списки в будь-якому разі не підлягають зміні.
juanpa.arrivillaga

3

Знадобилося трохи часу, щоб ознайомитися з higher order functions filterі map. Тож я звик до них, і мені насправді сподобалось, filterоскільки було очевидно, що він фільтрує, зберігаючи все, що є правдою, і мені було здорово, що я знав деякі functional programmingтерміни.

Потім я прочитав цей уривок (Fluent Python Book):

Функції карти та фільтра все ще є вбудованими в Python 3, але, оскільки введення розуміння списків та генераторних виразів, вони не є настільки важливими. Listcomp або genexp виконує завдання з картографуванням та фільтруванням у поєднанні, але є більш читабельним.

І тепер я думаю, навіщо турбуватися з концепцією filter/ mapякщо ви можете досягти цього за допомогою вже широко розповсюджених ідіом, як спискові розуміння. Крім того , mapsі filtersце свого роду функцій. У цьому випадку я вважаю за краще використовувати Anonymous functionsлямбда.

Нарешті, лише для того, щоб протестувати, я приуротив обидва методи ( mapі listComp), і не побачив жодної відповідної різниці швидкостей, яка б виправдовувала аргументи щодо цього.

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

0

Цікаво, що на Python 3 я бачу, що фільтр працює швидше, ніж розуміння списку.

Я завжди думав, що розуміння списку буде більш ефективним. Щось на кшталт: [ім'я для імені в brand_names_db, якщо ім'я немає None] Створений байт-код трохи кращий.

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

Але вони насправді повільніші:

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

8
Неправильне порівняння . По-перше, ви не передаєте лямбда-функцію версії фільтра, що робить її за замовчуванням функцією ідентичності. При визначенні if not Noneв списку розумінні ви є визначенням функції лямбда (зверніть увагу на MAKE_FUNCTIONзаяву). По-друге, результати різні, оскільки версія розуміння списку видалить лише Noneзначення, тоді як версія фільтра видалить усі "хибні" значення. Зважаючи на це, вся мета мікробізнетування марна. Це один мільйон ітерацій, разів 1 к! Різниця незначна .
Віктор Шредер

-7

Моє взяти

def filter_list(list, key, value, limit=None):
    return [i for i in list if i[key] == value][:limit]

3
iніколи не говорили, що це було dict, і в цьому немає потреби limit. Крім цього, чим це відрізняється від запропонованого ОП і як він відповідає на питання?
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.