Понимание списка против лямбда + фильтр
Я обнаружил, что у меня есть базовая потребность в фильтрации: у меня есть список, и я должен отфильтровать его по атрибуту элементов.
Мой код выглядел так:
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 вместо лямбды)?
18 ответов
Странно, сколько красоты у разных людей. Я считаю, что понимание списка намного яснее, чем filter
+ lambda
, но используйте то, что вы найдете проще. Однако, прекратите давать имена переменных, которые уже используются для встроенных модулей, это сбивает с толку. [ Вопрос первоначально использовался list
как имя переменной, но был обновлен в ответ на этот ответ. ]
Есть две вещи, которые могут замедлить ваше использование 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
Затем в вашем основном коде (где читаемость действительно имеет значение) вы заменили как понимание списка, так и фильтр на многообещающее имя функции.
Это несколько религиозная проблема в Python. Хотя Гвидо решил удалить map
, filter
а также reduce
от Python 3, было достаточно обратной реакции, что в конце только reduce
был перемещен из встроенных в functools.reduce.
Лично я нахожу список понимания легче читать. Это более четко, что происходит из выражения [i for i in list if i.attribute == value]
так как все поведение на поверхности, а не внутри функции фильтра.
Я бы не стал сильно беспокоиться о разнице в производительности между этими двумя подходами, поскольку она незначительна. Я бы действительно оптимизировал это, только если это оказалось узким местом в вашем приложении, что маловероятно.
Кроме того, так как BDFL хотел filter
ушел от языка, то, конечно, это автоматически делает списки понимания более Pythonic;-)
Поскольку любая разница в скорости обязательно будет крошечной, использовать фильтры или составлять списки - дело вкуса. В целом я склонен использовать понимание (что, похоже, согласуется с большинством других ответов здесь), но есть один случай, когда я предпочитаю 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)])
Хотя filter
может быть "более быстрый путь", "Pythonic way" - не заботиться о таких вещах, если производительность не является абсолютно критической (в этом случае вы не будете использовать Python!).
Я подумал, что просто добавлю, что в 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(), а также исходный список.
Важным отличием является то, что понимание списка вернет list
в то время как фильтр возвращает filter
которым вы не можете манипулировать как list
(т.е. звоните len
на нем, который не работает с возвратом filter
).
Мое самообучение привело меня к некоторой аналогичной проблеме.
При этом, если есть способ получить в результате list
из filter
немного, как вы сделали бы в.NET, когда вы делаете lst.Where(i => i.something()).ToList()
Мне интересно это знать.
РЕДАКТИРОВАТЬ: Это относится к Python 3, а не 2 (см. Обсуждение в комментариях).
Я считаю второй способ более читабельным. Он точно скажет вам, что вы хотите: отфильтруйте список.
PS: не используйте "список" в качестве имени переменной
В общем-то filter
немного быстрее, если использовать встроенную функцию.
Я ожидаю, что понимание списка будет немного быстрее в вашем случае
Фильтр это просто так. Он отфильтровывает элементы списка. Вы можете видеть, что определение упоминает то же самое (в официальной ссылке на документы, которую я упоминал ранее). Принимая во внимание, что понимание списка - это то, что создает новый список после воздействия на что-то в предыдущем списке.(И фильтрация, и понимание списка создают новый список и не выполняют операции вместо старого списка. Новый список здесь является чем-то вроде списка с скажем, совершенно новый тип данных. Как преобразование целых чисел в строку и т. д.)
В вашем примере лучше использовать фильтр, чем понимание списка, согласно определению. Однако, если вы хотите, скажем, other_attribute из элементов списка, в вашем примере вы должны получить новый список, тогда вы можете использовать понимание списка.
return [item.other_attribute for item in my_list if item.attribute==value]
Вот как я на самом деле помню о фильтрах и списках. Удалите несколько вещей из списка и оставьте остальные элементы без изменений, используйте фильтр. Используйте некоторую логику самостоятельно для элементов и создайте разбавленный список, подходящий для какой-то цели, используйте понимание списка.
Вот небольшой фрагмент, который я использую, когда мне нужно отфильтровать что-то после понимания списка. Просто комбинация фильтра, лямбды и списков (иначе называемых верностью кошки и чистотой собаки).
В этом случае я читаю файл, удаляю пустые строки, закомментированные строки и все, что угодно после комментария к строке:
# 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])]
Мне потребовалось некоторое время, чтобы ознакомиться с 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
С точки зрения производительности, это зависит.
возвращает не список, а итератор, если вам нужна немедленная фильтрация списка и преобразование списка, это медленнее, чем с пониманием списка, примерно на 40% для очень больших списков (>1M). До 100К элементов разницы почти нет, начиная с 600К начинаются отличия.
Если вы не конвертируете в список,
filter
происходит практически мгновенно.
Дополнительная информация по адресу: https://blog.finxter.com/python-lists-filter-vs-list-comprehension-what-is-faster/
В дополнение к принятому ответу, есть угловой случай, когда вы должны использовать фильтр вместо понимания списка. Если список не подлежит изменению, вы не можете напрямую обработать его с помощью понимания списка. Пример из реального мира, если вы используете pyodbc
читать результаты из базы данных. fetchAll()
результаты из cursor
это нерушимый список. В этой ситуации, чтобы напрямую манипулировать возвращаемыми результатами, следует использовать фильтр:
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 тип: 'список'
Я бы пришел к выводу: используйте понимание списка вместо фильтра, поскольку его
- более читаемый
- более питонический
- быстрее (для Python 3.11 см. прилагаемый бенчмарк, также см. )
Имейте в виду, что filter возвращает итератор, а не список.
python3 -m timeit '[x for x in range(10000000) if x % 2 == 0]'
1 цикл, лучший из 5: 270 мс на цикл
python3 -m timeit 'list(filter(lambda x: x % 2 == 0, range(10000000)))'
1 цикл, лучший из 5: 432 мс на цикл
Любопытно, что в 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
Обобщение других ответов
Просматривая ответы, мы видели много вопросов о том, может ли понимание списка или фильтр быть быстрее или даже важно или питонично заботиться о такой проблеме. В конце концов, ответ, как и в большинстве случаев: это зависит.
Я просто наткнулся на этот вопрос при оптимизации кода, где именно этот вопрос (хотя и в сочетании с
in
выражение, не
==
) очень актуально -
filter
+
lambda
выражение занимает треть моего времени вычислений (несколько минут).
Мое дело
В моем случае понимание списка происходит намного быстрее (в два раза быстрее). Но я подозреваю, что это сильно зависит от выражения фильтра, а также от используемого интерпретатора Python.
Проверьте это на себе
Вот простой фрагмент кода, который легко адаптировать. Если вы профилируете его (большинство IDE могут сделать это легко), вы сможете легко решить для своего конкретного случая, какой вариант лучше:
whitelist = set(range(0, 100000000, 27))
input_list = list(range(0, 100000000))
proximal_list = list(filter(
lambda x: x in whitelist,
input_list
))
proximal_list2 = [x for x in input_list if x in whitelist]
print(len(proximal_list))
print(len(proximal_list2))
Если у вас нет IDE, позволяющей легко профилировать, попробуйте это (извлечено из моей кодовой базы, так что это немного сложнее). Этот фрагмент кода создаст для вас профиль, который вы можете легко визуализировать, используя, например , змеиную визуализацию :
import cProfile
from time import time
class BlockProfile:
def __init__(self, profile_path):
self.profile_path = profile_path
self.profiler = None
self.start_time = None
def __enter__(self):
self.profiler = cProfile.Profile()
self.start_time = time()
self.profiler.enable()
def __exit__(self, *args):
self.profiler.disable()
exec_time = int((time() - self.start_time) * 1000)
self.profiler.dump_stats(self.profile_path)
whitelist = set(range(0, 100000000, 27))
input_list = list(range(0, 100000000))
with BlockProfile("/path/to/create/profile/in/profile.pstat"):
proximal_list = list(filter(
lambda x: x in whitelist,
input_list
))
proximal_list2 = [x for x in input_list if x in whitelist]
print(len(proximal_list))
print(len(proximal_list2))
Ваш вопрос такой простой, но интересный. Это просто показывает, насколько гибок Python как язык программирования. Можно использовать любую логику и написать программу в соответствии со своим талантом и пониманием. Это нормально, пока мы получаем ответ.
Здесь, в вашем случае, это всего лишь простой метод фильтрации, который может выполняться обоими, но я бы предпочел первый.
my_list = [x for x in my_list if x.attribute == value]
потому что это кажется простым и не требует специального синтаксиса. Любой может понять эту команду и внести изменения, если это необходимо. (Хотя второй способ тоже простой, но он все же сложнее первого для программистов начального уровня)
Мой дубль
def filter_list(list, key, value, limit=None):
return [i for i in list if i[key] == value][:limit]