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

Есть ли причина предпочесть использовать 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 и 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)).

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

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

Ответ 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 для определения общего объема памяти, необходимого классу (атрибутам), уменьшая объем памяти. Вот итоговые сюжеты.

Performance of mapping Python object methods

Как указывалось ранее, используемая техника имеет минимальное значение, и вы должны кодировать ее наиболее удобным для вас образом или в определенных обстоятельствах. В этом случае понимание списка (техника map_comprehension) наиболее быстро для обоих типов дополнений в объекте, особенно с более короткими списками.

Посетите этот каталог для источника, использованного для создания графика и данных.