понимание списка против лямбда + фильтр


859

Я обнаружил, что у меня есть базовая потребность в фильтрации: у меня есть список, и я должен отфильтровать его по атрибуту элементов.

Мой код выглядел так:

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является объектом генератора фильтров, а не списком.
Маттео

Ответы:


589

Странно, насколько различна красота у разных людей. Я считаю, что понимание списка намного яснее, чем 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 да, возможно, мне было неясно. Я пытался сказать, что в основном коде вы должны видеть более широкую картину. В маленькой вспомогательной функции вам нужно заботиться только об этой функции, то, что еще происходит снаружи, можно игнорировать.
Дункан

238

Это несколько религиозная проблема в Python. Несмотря на то, что Гвидо рассматривал возможность удаления map, filterи reduceиз Python 3 было достаточно негативной реакции, которая в итоге reduceбыла перемещена только из встроенных модулей в functools.reduce .

Лично я нахожу список понимания легче читать. Более явно то, что происходит из выражения, [i for i in list if i.attribute == value]поскольку все поведение находится на поверхности, а не внутри функции фильтра.

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

Кроме того, так как BDFL хотел filterуйти от языка, то, конечно, это автоматически делает списки более Pythonic ;-)


1
Спасибо за ссылки на комментарии Гвидо, если для меня больше ничего не значит, я постараюсь больше их не использовать, чтобы не приобрести привычку и не стал бы поддерживать эту религию :)
Дашеси

1
но уменьшить это самое сложное, что можно сделать с помощью простых инструментов! карту и фильтр тривиально заменить на понимания!
njzk2

8
не знал, что снижение было понижено в должности в Python3. спасибо за понимание! Reduce () все еще весьма полезен в распределенных вычислениях, таких как PySpark. Я думаю, что это было ошибкой ..
Тагар

1
@Tagar, вы все еще можете использовать «уменьшить», вам просто нужно импортировать его из
functools

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

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

3
Обратите внимание, что мы можем использовать выражение генератора вместо этого в последнем примере, если мы не хотим использовать память:prime_cubes = filter(prime, (x*x*x for x in range(1000)))
Mateen Ulhaq

4
@MateenUlhaq это можно оптимизировать, чтобы prime_cubes = [1]сэкономить память и циклы процессора ;-)
Деннис Крупеник

7
@DennisKrupenik Вернее,[]
Матеин Улхак,

29

Хотя это filterможет быть «более быстрый путь», «Pythonic way» не заботится о таких вещах, если производительность не является абсолютно критической (в этом случае вы не будете использовать Python!).


10
Поздний комментарий к часто встречающемуся аргументу: иногда имеет смысл запускать анализ за 5 часов вместо 10, и если этого можно достичь, потратив на оптимизацию кода Python один час, это может стоить (особенно если комфортно с питоном, а не с более быстрыми языками).
ло

Но более важно то, насколько сильно исходный код замедляет нас, пытаясь прочитать и понять это!
thoni56

20

Я подумал, что просто добавлю, что в python 3 filter () на самом деле является объектом итератора, поэтому вам нужно передать вызов метода 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 в 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 для x в y, если z], потому что вам приходится перебирать выходные данные filter (), а также исходный список.


13

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

Мое самообучение привело меня к некоторой аналогичной проблеме.

Это , как говорится, если есть способ , чтобы полученный listот А filter, немного , как вы могли бы сделать в .NET , когда вы делаете lst.Where(i => i.something()).ToList(), мне интересно знать.

РЕДАКТИРОВАТЬ: Это относится к Python 3, а не 2 (см. Обсуждение в комментариях).


4
Фильтр возвращает список, и мы можем использовать len на нем. По крайней мере, в моем Python 2.7.6.
triuvenkadam

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: не используйте «список» в качестве имени переменной


7

как правило filter, немного быстрее, если использовать встроенную функцию.

Я ожидаю, что понимание списка будет немного быстрее в вашем случае


python -m timeit 'filter (lambda x: x in [1,2,3,4,5], range (10000000))' 10 циклов, лучшее из 3: 1,44 с на цикл Python -m timeit '[x для x в диапазоне (10000000), если x в [1,2,3,4,5]] '10 циклов, лучшее из 3: 860 мсек на цикл Не совсем ?!
giaosudau

@sepdau, лямбда-функции не являются встроенными. Понимание списка улучшилось за последние 4 года - теперь разница в любом случае незначительна даже со встроенными функциями
Джон Ла Рой

7

Фильтр это просто так. Он отфильтровывает элементы списка. Вы можете видеть, что определение упоминает то же самое (в официальной ссылке на документы, которую я упоминал ранее). Принимая во внимание, что понимание списка - это то, что создает новый список после воздействия на что-то из предыдущего списка. (И фильтрация, и понимание списка создают новый список и не выполняют операции вместо старого списка. Новый список здесь представляет собой что-то вроде списка с скажем, совершенно новый тип данных. Как преобразование целых чисел в строку и т. д.)

В вашем примере, лучше использовать фильтр, чем понимание списка, согласно определению. Однако, если вы хотите, скажем, other_attribute из элементов списка, в вашем примере вы должны получить новый список, тогда вы можете использовать понимание списка.

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

Вот как я на самом деле помню о фильтрах и списках. Удалите несколько вещей из списка и оставьте остальные элементы нетронутыми, используйте фильтр. Используйте некоторую логику самостоятельно для элементов и создайте разбавленный список, подходящий для какой-то цели, используйте понимание списка.


2
Я буду рад узнать причину отрицательного голосования, чтобы больше не повторять его нигде в будущем.
triuvenkadam

определение фильтра и понимание списка не были необходимы, поскольку их значение не обсуждалось. То, что понимание списка должно использоваться только для «новых» списков, представлено, но не аргументировано.
Agos

Я использовал определение, чтобы сказать, что фильтр дает вам список с теми же элементами, которые верны для случая, но с пониманием списка мы можем изменить сами элементы, например, преобразовать int в str. Но точка взята :-)
thiruvenkadam

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 тип: 'список'


2
все списки не подлежат изменению, >>> 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]
Томас Грейнджер

1
«Если список не подлежит изменению, вы не можете напрямую обработать его с помощью понимания списка». Это не так, и все списки в любом случае не подлежат отмене.
juanpa.arrivillaga

3

Мне потребовалось некоторое время, чтобы ознакомиться с higher order functions filterи map. Так что я привык к ним, и мне действительно понравилось, filterтак как было ясно, что оно фильтрует, сохраняя все правдивое, и я чувствовал себя классно, что я знал некоторыеfunctional programming термины.

Затем я прочитал этот отрывок (Свободная Книга Питона):

Функции отображения и фильтрации по-прежнему встроены в 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 фильтр работает быстрее, чем списки.

Я всегда думал, что понимание списка будет более производительным. Примерно так: [имя для name в brand_names_db, если name не 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.