Понимание списка против карты - PullRequest
638 голосов
/ 08 августа 2009

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

Ответы [ 9 ]

592 голосов
/ 08 августа 2009

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

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

$ 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
420 голосов
/ 20 июня 2011

Случаи

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

def sumEach(myLists):
    return [sum(_) for _ in myLists]
  • Просто используя for -loop : Конечно, вы также можете просто использовать цикл for. Хотя это не так элегантно с точки зрения функционального программирования, иногда нелокальные переменные делают код более понятным в императивных языках программирования, таких как python, потому что люди очень привыкли читать код таким образом. Циклы for также, как правило, наиболее эффективны, когда вы просто выполняете какую-либо сложную операцию, которая не строит список, например, списки и карты оптимизированы (например, суммирование, создание дерева и т. Д.) - по крайней мере эффективный с точки зрения памяти (не обязательно с точки зрения времени, где в худшем случае я бы ожидал постоянного фактора, за исключением некоторого редкого патологического сбоя при сборке мусора).

"Pythonism"

Мне не нравится слово "pythonic", потому что я не считаю, что 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 немного непрозрачен для разборки, но мы можем сделать это с помощью нашего теста скорости.

88 голосов
/ 21 ноября 2012

Вы должны использовать 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), если они подходят для вашей ситуации!

39 голосов
/ 01 октября 2013

На самом деле, 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)).

Подведем итог:

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

Я считаю, что списочные понимания, как правило, более выразительны из того, что я пытаюсь сделать, чем map - они оба выполняют это, но первое избавляет от умственной нагрузки, пытаясь понять, что может быть сложным lambda выражением .

Также есть где-то интервью (я не могу найти его от руки), где Гвидо перечисляет lambda s и функциональные функции как то, что он больше всего сожалеет о принятии в Python, так что вы можете сделать аргумент, что они ' в силу этого непифония.

15 голосов
/ 08 июня 2014

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

15 голосов
/ 02 ноября 2009

Вот один из возможных случаев:

map(lambda op1,op2: op1*op2, list1, list2)

против

[op1*op2 for op1,op2 in zip(list1,list2)]

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

6 голосов
/ 03 декабря 2016

Итак, так как Python 3, map() является итератором, вам нужно помнить, что вам нужно: итератор или list объект.

Как @AlexMartelli уже упомянул , map() быстрее, чем списки, только если вы не используете функцию lambda.

Я представлю вам некоторые сравнения времени.

Python 3.5.2 и CPython
Я использовал блокнот Юпитера и особенно %timeit встроенную магическую команду
Измерения : с == 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()!

0 голосов
/ 04 сентября 2017

Я считаю, что самый 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, чтобы разрешить фильтрацию.

...