Вопрос-Ответ

List comprehension vs map

Понимание списка против карты

Есть ли причина предпочесть использование map() пониманию списка или наоборот? Любой из них в целом более эффективен или считается более питонистским, чем другой?

Переведено автоматически
Ответ 1

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

Пример крошечного преимущества в скорости map при использовании точно такой же функции:

$ python -m timeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop

$ python -m timeit -s'xs=range(10)' '[hex(x) for x in xs]'
100000 loops, best of 3: 5.58 usec per loop

Пример того, как сравнение производительности полностью меняется на противоположное, когда map требуется лямбда:

$ python -m timeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop

$ python -m timeit -s'xs=range(10)' '[x+2 for x in xs]'
100000 loops, best of 3: 2.32 usec per loop
Ответ 2

Примеры


  • Распространенный случай: почти всегда вам захочется использовать понимание списка в python, потому что начинающим программистам, читающим ваш код, будет более очевидно, что вы делаете. (Это не относится к другим языкам, где могут применяться другие идиомы.) Для программистов на Python будет даже более очевидно, что вы делаете, поскольку понимание списка является стандартом де-факто в python для итераций; они ожидаемы.

  • Менее распространенный случай: Однако, если у вас уже есть определенная функция, часто разумно использовать map, хотя это считается "непитоническим". Например, map(sum, myLists) более элегантный / краткий, чем [sum(x) for x in myLists]. Вы получаете элегантность от того, что вам не нужно создавать фиктивную переменную (например, sum(x) for x... or sum(_) for _... или sum(readableName) for readableName...), которую вам приходится вводить дважды, просто для повторения. Тот же аргумент справедлив для filter and reduce и всего, что находится в itertools модуле: если у вас уже есть удобная функция, вы могли бы продолжить и заняться функциональным программированием. Это улучшает читаемость в одних ситуациях и теряет ее в других (например, начинающие программисты, множество аргументов)... но читаемость вашего кода в любом случае сильно зависит от ваших комментариев.

  • Почти никогда: вы можете захотеть использовать map функцию как чисто абстрактную функцию при выполнении функционального программирования, где вы выполняете отображение map, или каррирование map, или иным образом извлекаете выгоду из разговоров о map как функции. Например, в Haskell интерфейс функтора под названием fmap обобщает отображение по любой структуре данных. Это очень необычно в python, потому что грамматика python вынуждает вас использовать стиль генератора, чтобы говорить об итерации; вы не можете легко обобщить это. (Иногда это хорошо, а иногда плохо.) Вероятно, вы можете найти редкие примеры python, где map(f, *lists) это разумный поступок. Самым близким примером, который я могу придумать, был бы sumEach = partial(map,sum), который представляет собой однострочник, который очень приблизительно эквивалентен:

def sumEach(myLists):
return [sum(_) for _ in myLists]

  • Просто используя forцикл: вы также, конечно, можете просто использовать цикл for. Хотя это и не так элегантно с точки зрения функционального программирования, иногда нелокальные переменные делают код более понятным в императивных языках программирования, таких как python, потому что люди очень привыкли читать код таким образом. Циклы For также, как правило, наиболее эффективны, когда вы просто выполняете какую-либо сложную операцию, которая не заключается в создании списка, для чего оптимизированы понимание списка и карта (например, суммирование или построение дерева и т.д.) - По крайней мере, эффективны с точки зрения памяти (не обязательно с точки зрения времени, где я ожидал бы в худшем случае постоянного фактора, за исключением некоторых редких патологических сбоев при сборке мусора).

"Питонизм"

Мне не нравится слово "pythonic", потому что я не нахожу, что pythonic всегда элегантен в моих глазах. Тем не менее, map and filter и подобные функции (например, очень полезный itertools модуль), вероятно, считаются непитоническими с точки зрения стиля.

Лень

С точки зрения эффективности, как и большинство конструкций функционального программирования, MAP МОЖЕТ БЫТЬ ЛЕНИВЫМ, и на самом деле он ленив в python. Это означает, что вы можете сделать это (в python3), и на вашем компьютере не закончится память и не потеряются все ваши несохраненные данные:

>>> map(str, range(10**100))
<map object at 0x2201d50>

Попробуйте сделать это с пониманием списка:

>>> [str(n) for n in range(10**100)]
# DO NOT TRY THIS AT HOME OR YOU WILL BE SAD #

Обратите внимание, что понимание списков также по своей сути лениво, но python решил реализовать их как не ленивые. Тем не менее, python поддерживает понимание отложенного списка в форме выражений генератора следующим образом:

>>> (str(n) for n in range(10**100))
<generator object <genexpr> at 0xacbdef>

В принципе, вы можете представить себе [...] синтаксис как передачу выражения генератора конструктору списка, например list(x for x in range(5)).

Краткий надуманный пример

from operator import neg
print({x:x**2 for x in map(neg,range(5))})

print({x:x**2 for x in [-y for y in range(5)]})

print({x:x**2 for x in (-y for y in range(5))})

Понимание списка не требует ленивости, поэтому может потребовать больше памяти (если вы не используете понимание генератора). Квадратные скобки [...] часто делают вещи очевидными, особенно когда они заключены в беспорядочные круглые скобки. С другой стороны, иногда вы в конечном итоге становитесь многословным, как при наборе текста [x for x in.... Пока вы сохраняете краткость переменных итератора, понимание списка обычно становится более понятным, если вы не делаете отступы в своем коде. Но вы всегда можете сделать отступ в своем коде.

print(
{x:x**2 for x in (-y for y in range(5))}
)

или разбейте все на части:

rangeNeg5 = (-y for y in range(5))
print(
{x:x**2 for x in rangeNeg5}
)

Сравнение эффективности для python3

map теперь ленив:

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=map(f,xs)'
1000000 loops, best of 3: 0.336 usec per loop ^^^^^^^^^

Поэтому, если вы не будете использовать все свои данные или не знаете заранее, какой объем данных вам нужен, map в python3 (и выражениях генератора в python2 или python3) будет избегать вычисления их значений до последнего необходимого момента. Обычно это перевешивает любые накладные расходы от использования map. Недостатком является то, что это очень ограничено в python в отличие от большинства функциональных языков: вы получаете это преимущество, только если обращаетесь к своим данным слева направо "по порядку", потому что выражения генератора python могут оцениваться только в порядке x[0], x[1], x[2], ....

Однако давайте предположим, что у нас есть готовая функция, f которую мы хотели бы map использовать, и мы игнорируем лень map, немедленно принудительно выполняя вычисление с помощью list(...). Мы получаем несколько очень интересных результатов:

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(map(f,xs))'                                                                                                                                                
10000 loops, best of 3: 165/124/135 usec per loop ^^^^^^^^^^^^^^^
for list(<map object>)

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=[f(x) for x in xs]'
10000 loops, best of 3: 181/118/123 usec per loop ^^^^^^^^^^^^^^^^^^
for list(<generator>), probably optimized

% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(f(x) for x in xs)'
1000 loops, best of 3: 215/150/150 usec per loop ^^^^^^^^^^^^^^^^^^^^^^
for list(<generator>)

Результаты In представлены в виде AAA / BBB / CCC, где A было выполнено на рабочей станции Intel 2010 года выпуска с python 3.?.?, а B и C были выполнены на рабочей станции AMD 2013 года выпуска с python 3.2.1 на совершенно другом оборудовании. В результате, похоже, что понимание карты и списка сопоставимо по производительности, на которую наиболее сильно влияют другие случайные факторы. Единственное, что мы можем сказать, это то, что, как ни странно, хотя мы ожидаем, что понимание списка [...] будет работать лучше, чем выражения генератора (...), map также более эффективно, чем выражения генератора (опять же при условии, что все значения вычисляются / используются).

Важно понимать, что эти тесты предполагают очень простую функцию (функцию идентификации); однако это нормально, потому что если бы функция была сложной, то накладные расходы на производительность были бы незначительными по сравнению с другими факторами в программе. (Все еще может быть интересно протестировать с другими простыми вещами, такими как f=lambda x:x+x)

Если вы умеете читать ассемблер python, вы можете использовать dis модуль, чтобы увидеть, действительно ли это происходит за кулисами:

>>> listComp = compile('[f(x) for x in xs]', 'listComp', 'eval')
>>> dis.dis(listComp)
1 0 LOAD_CONST 0 (<code object <listcomp> at 0x2511a48, file "listComp", line 1>)
3 MAKE_FUNCTION 0
6 LOAD_NAME 0 (xs)
9 GET_ITER
10 CALL_FUNCTION 1
13 RETURN_VALUE
>>> listComp.co_consts
(<code object <listcomp> at 0x2511a48, file "listComp", line 1>,)
>>> dis.dis(listComp.co_consts[0])
1 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 18 (to 27)
9 STORE_FAST 1 (x)
12 LOAD_GLOBAL 0 (f)
15 LOAD_FAST 1 (x)
18 CALL_FUNCTION 1
21 LIST_APPEND 2
24 JUMP_ABSOLUTE 6
>> 27 RETURN_VALUE

 

>>> listComp2 = compile('list(f(x) for x in xs)', 'listComp2', 'eval')
>>> dis.dis(listComp2)
1 0 LOAD_NAME 0 (list)
3 LOAD_CONST 0 (<code object <genexpr> at 0x255bc68, file "listComp2", line 1>)
6 MAKE_FUNCTION 0
9 LOAD_NAME 1 (xs)
12 GET_ITER
13 CALL_FUNCTION 1
16 CALL_FUNCTION 1
19 RETURN_VALUE
>>> listComp2.co_consts
(<code object <genexpr> at 0x255bc68, file "listComp2", line 1>,)
>>> dis.dis(listComp2.co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 17 (to 23)
6 STORE_FAST 1 (x)
9 LOAD_GLOBAL 0 (f)
12 LOAD_FAST 1 (x)
15 CALL_FUNCTION 1
18 YIELD_VALUE
19 POP_TOP
20 JUMP_ABSOLUTE 3
>> 23 LOAD_CONST 0 (None)
26 RETURN_VALUE

 

>>> evalledMap = compile('list(map(f,xs))', 'evalledMap', 'eval')
>>> dis.dis(evalledMap)
1 0 LOAD_NAME 0 (list)
3 LOAD_NAME 1 (map)
6 LOAD_NAME 2 (f)
9 LOAD_NAME 3 (xs)
12 CALL_FUNCTION 2
15 CALL_FUNCTION 1
18 RETURN_VALUE

Кажется, лучше использовать [...] синтаксис, чем list(...). К сожалению, map класс немного непрозрачен для дизассемблирования, но мы можем сделать это с помощью нашего теста скорости.

Ответ 3

Python 2: вы должны использовать map и filter вместо понимания списка.

Объективная причина, по которой вы должны предпочесть их, даже если они не "Pythonic", заключается в следующем:
они требуют функций / лямбд в качестве аргументов, которые вводят новую область видимости.

Я сталкивался с этим не раз:

for x, y in somePoints:
# (several lines of code here)
squared = [x ** 2 for x in numbers]
# Oops, x was silently overwritten!

но если бы вместо этого я сказал:

for x, y in somePoints:
# (several lines of code here)
squared = map(lambda x: x ** 2, numbers)

тогда все было бы хорошо.

Вы могли бы сказать, что я вел себя глупо, используя одно и то же имя переменной в одной и той же области видимости.

Я не был. Изначально код был в порядке - два xs не были в одной области видимости.
Проблема возникла только после того, как я переместил внутренний блок в другой раздел кода (читай: проблема во время обслуживания, а не разработки), и я этого не ожидал.

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

Заключение:

Используйте map и filter. Они предотвращают малозаметные, трудно диагностируемые ошибки, связанные с областью видимости.

Дополнительное примечание:

Не забудьте рассмотреть возможность использования imap и ifilteritertools), если они подходят для вашей ситуации!

Ответ 4

На самом деле, map и понимание списка ведут себя совершенно по-разному в языке Python 3. Взгляните на следующую программу на Python 3:

def square(x):
return x*x
squares = map(square, [1, 2, 3])
print(list(squares))
print(list(squares))

Вы могли бы ожидать, что он дважды напечатает строку "[1, 4, 9]", но вместо этого он печатает "[1, 4, 9]", за которым следует "[]". При первом взгляде на squares кажется, что он ведет себя как последовательность из трех элементов, но во второй раз как пустой.

На языке Python 2 map возвращает простой старый список, точно так же, как понимание списка происходит на обоих языках. Суть в том, что возвращаемое значение map в Python 3 (и imap в Python 2) не является списком - это итератор!

Элементы используются при выполнении итерации по итератору, в отличие от того, когда вы выполняете итерацию по списку. Вот почему squares выглядит пустой в последней print(list(squares)) строке.

Подводя итог:


  • При работе с итераторами вы должны помнить, что они отслеживают состояние и что они изменяются по мере прохождения по ним.

  • Списки более предсказуемы, поскольку они меняются только тогда, когда вы явно изменяете их; они являются контейнерами.

  • И бонус: числа, строки и кортежи еще более предсказуемы, поскольку они вообще не могут изменяться; они являются значениями.

python