Понимание списка против карты
Есть ли причина предпочесть использовать map()
для понимания списка или наоборот? Является ли каждый из них более эффективным или считается более питоническим, чем другой?
Ответы
Ответ 1
map
может быть микроскопически быстрее в некоторых случаях (когда вы НЕ делаете лямбда для этой цели, но используете ту же функцию на карте и listcomp). Учет списков может быть быстрее в других случаях, и большинство (не все) pythonistas считают их более прямыми и понятными.
Пример крошечного преимущества карты при использовании точно такой же функции:
$ python -mtimeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop
$ python -mtimeit -s'xs=range(10)' '[hex(x) for x in xs]'
100000 loops, best of 3: 5.58 usec per loop
Пример того, как сравнение производительности полностью отменяется, когда карта нуждается в лямбда:
$ python -mtimeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop
$ python -mtimeit -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...
или sum(_) for _...
или sum(readableName) for readableName...
), которую вы должны ввести дважды, просто для итерации. Тот же аргумент имеет значение для filter
и reduce
и что-либо из модуля itertools
: если у вас уже есть функция, вы можете продолжить и выполнить некоторое функциональное программирование. Это повышает удобочитаемость в некоторых ситуациях и теряет его в других (например, начинающие программисты, несколько аргументов)... но читаемость вашего кода сильно зависит от ваших комментариев. - Почти никогда: вы можете использовать функцию
map
как чистую абстрактную функцию, выполняя функциональное программирование, когда вы набираете map
или каррируете map
, или иным образом получаете возможность говорить о map
как функции. Например, в Haskell интерфейс functor, называемый fmap
обобщает отображение по любой структуре данных. Это очень редко встречается в python, потому что грамматика python заставляет вас использовать генераторный стиль, чтобы говорить об итерации; вы не можете легко обобщить его. (Иногда это бывает хорошо, а иногда и плохо.) Вероятно, вы можете найти редкие примеры python, где map(f, *lists)
является разумной вещью. Самый близкий пример, который я могу придумать, будет sumEach = partial(map,sum)
, который является однострочным, который очень примерно эквивалентен:
def sumEach(myLists):
return [sum(_) for _ in myLists]
- Просто используя
for
-loop: вы также можете, конечно, просто использовать для -loop. Хотя не так элегантно с точки зрения функционального программирования, иногда нелокальные переменные делают код более ясным в императивных языках программирования, таких как python, потому что люди очень привыкли к чтению кода таким образом. Для -loop s также, как правило, наиболее эффективны, когда вы просто выполняете какую-либо сложную операцию, которая не создает список, такой как списки и карты, оптимизированы для (например, суммирования или создания дерева и т.д.) - по крайней мере, эффективный с точки зрения памяти (не обязательно с точки зрения времени, где я ожидал бы в худшем случае постоянный фактор, за исключением некоторых редких патологических сборов мусора).
"Pythonism"
Мне не нравится слово "pythonic", потому что я не нахожу, что в моих глазах всегда бывает элегантно. Тем не менее, map
и 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 в отличие от большинства функциональных языков: вы получаете это преимущество только в том случае, если вы получаете доступ к вашим данным слева направо "по порядку", потому что выражения генератора питона могут быть оценены только по порядку 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>)
Результаты представлены в виде 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)
тогда все было бы хорошо.
Можно сказать, что я глуп, что использовал одно и то же имя переменной в той же области видимости.
Я не был Изначально код был в порядке - два x
были не в одной области видимости.
Только после того, как я переместил внутренний блок в другой раздел кода, возникла проблема (читай: проблема во время обслуживания, а не разработки), и я этого не ожидал.
Да, если вы никогда не совершите эту ошибку, тогда списочные представления будут более элегантными.
Но из личного опыта (и из-за того, что другие делают ту же ошибку), я видел, что такое случалось достаточно часто, и я думаю, что это не стоит той боли, которую вам придется пережить, когда эти ошибки проникают в ваш код.
Заключение:
Используйте map
и filter
. Они предотвращают тонкие, трудно диагностируемые ошибки, связанные с областью действия.
Примечание:
Не забудьте рассмотреть возможность использования imap
и ifilter
(в itertools
), если они подходят для вашей ситуации!
Ответ 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))
.
Подводя итог:
- При работе с итераторами вы должны помнить, что они являются состояниями, и что они мутируют, когда вы проходите их.
- Списки более предсказуемы, поскольку они изменяются только при явной их мутации; они являются контейнерами.
- И бонус: числа, строки и кортежи еще более предсказуемы, поскольку они не могут вообще меняться; они являются значениями.
Ответ 5
Я нахожу, что понимание списков, как правило, более выразительно из того, что я пытаюсь сделать, чем map
- они оба это делают, но первый сохраняет умственную нагрузку, пытаясь понять, что может быть сложным выражением lambda
.
Там также есть интервью где-то (я не могу найти его вслух), где Guido перечисляет lambda
, а функциональные функции - то, о чем он больше всего сожалеет о принятии на Python, поэтому вы можете сделать аргумент, что они в силу этого не-Pythonic.
Ответ 6
Вот один из возможных случаев:
map(lambda op1,op2: op1*op2, list1, list2)
против
[op1*op2 for op1,op2 in zip(list1,list2)]
Я предполагаю, что zip() является неудачным и ненужным накладными расходами, которые вам нужно потворствовать, если вы настаиваете на том, чтобы использовать списки, а не карту. Было бы здорово, если кто-то прояснит это, утвердительно или отрицательно.
Ответ 7
Если вы планируете писать какой-либо асинхронный, параллельный или распределенный код, вы, вероятно, предпочтете map
над пониманием списка, поскольку большинство асинхронных, параллельных или распределенных пакетов предоставляют функцию map
для перегрузки python map
. Затем, передав соответствующую функцию map
в остальную часть вашего кода, вам может не потребоваться изменить исходный серийный код, чтобы он выполнялся параллельно (и т.д.).
Ответ 8
Еще одна причина использовать понимание списков над map() и filter() заключается в том, что Psyco не может скомпилировать эти функции.
См. http://psyco.sourceforge.net/
Ответ 9
Итак, поскольку Python 3, map()
является итератором, вам нужно иметь в виду, что вам нужно: итератор или list
.
Как уже упоминалось @AlexMartelli , map()
быстрее, чем понимание списка, только если вы не используете функцию lambda
.
Я покажу вам несколько сравнений времени.
<суб > Python 3.5.2 и CPython
Я использовал Jupiter notebook и особенно %timeit
встроенная магическая команда
Измерения: s == 1000 мс == 1000 * 1000 мкс = 1000 * 1000 * 1000 нс
Настройка:
x_list = [(i, i+1, i+2, i*2, i-9) for i in range(1000)]
i_list = list(range(1000))
Встроенная функция:
%timeit map(sum, x_list) # creating iterator object
# Output: The slowest run took 9.91 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 277 ns per loop
%timeit list(map(sum, x_list)) # creating list with map
# Output: 1000 loops, best of 3: 214 µs per loop
%timeit [sum(x) for x in x_list] # creating list with list comprehension
# Output: 1000 loops, best of 3: 290 µs per loop
lambda
:
%timeit map(lambda i: i+1, i_list)
# Output: The slowest run took 8.64 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 325 ns per loop
%timeit list(map(lambda i: i+1, i_list))
# Output: 1000 loops, best of 3: 183 µs per loop
%timeit [i+1 for i in i_list]
# Output: 10000 loops, best of 3: 84.2 µs per loop
Существует также такая вещь, как выражение генератора, см. PEP-0289. Поэтому я подумал, что было бы полезно добавить его к сравнению
%timeit (sum(i) for i in x_list)
# Output: The slowest run took 6.66 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 495 ns per loop
%timeit list((sum(x) for x in x_list))
# Output: 1000 loops, best of 3: 319 µs per loop
%timeit (i+1 for i in i_list)
# Output: The slowest run took 6.83 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 506 ns per loop
%timeit list((i+1 for i in i_list))
# Output: 10000 loops, best of 3: 125 µs per loop
Вам нужен объект list
:
Использовать список, если это пользовательская функция, используйте list(map())
, если есть встроенная функция
Вам не нужен объект list
, вам просто нужно итеративное:
Всегда используйте map()
!
Ответ 10
Я считаю, что наиболее Pythonic-путь заключается в использовании понимания списка вместо map
и filter
. Причина в том, что понимание списков более четкое, чем map
и filter
.
In [1]: odd_cubes = [x ** 3 for x in range(10) if x % 2 == 1] # using a list comprehension
In [2]: odd_cubes_alt = list(map(lambda x: x ** 3, filter(lambda x: x % 2 == 1, range(10)))) # using map and filter
In [3]: odd_cubes == odd_cubes_alt
Out[3]: True
Как вы видите, понимание не требует дополнительных выражений lambda
в качестве map
. Кроме того, понимание также позволяет легко фильтровать, а для map
требуется filter
разрешить фильтрацию.
Ответ 11
Я провел быстрый тест, сравнивая три метода для вызова метода объекта. Разница во времени в этом случае незначительна и зависит от рассматриваемой функции (см. Ответ @Alex Martelli). Здесь я посмотрел на следующие методы:
# map_lambda
list(map(lambda x: x.add(), vals))
# map_operator
from operator import methodcaller
list(map(methodcaller("add"), vals))
# map_comprehension
[x.add() for x in vals]
Я просмотрел списки (хранящиеся в переменной vals
) как целых чисел (Python int
), так и чисел с плавающей запятой (Python float
) для увеличения размеров списков. Следующий фиктивный класс DummyNum
считается:
class DummyNum(object):
"""Dummy class"""
__slots__ = 'n',
def __init__(self, n):
self.n = n
def add(self):
self.n += 5
В частности, метод add
. __slots__
- это простая оптимизация в Python для определения общего объема памяти, необходимого классу (атрибутам), уменьшая объем памяти. Вот итоговые сюжеты.
Как указывалось ранее, используемая техника имеет минимальное значение, и вы должны кодировать ее наиболее удобным для вас образом или в определенных обстоятельствах. В этом случае понимание списка (техника map_comprehension
) наиболее быстро для обоих типов дополнений в объекте, особенно с более короткими списками.
Посетите этот каталог для источника, использованного для создания графика и данных.