Вопрос

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

Это было полезно?

Решение

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

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

$ 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

Другие советы

Случаи

  • Общий случай:Почти всегда вам захочется использовать понимание списка в питон потому что начинающим программистам, читающим ваш код, будет более очевидно, что вы делаете.(Это не относится к другим языкам, где могут применяться другие идиомы.) Программистам 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-петля:Вы также можете, конечно, просто использовать цикл for.Хотя с точки зрения функционального программирования это не так элегантно, иногда нелокальные переменные делают код более понятным в императивных языках программирования, таких как Python, потому что люди очень привыкли читать код таким образом.Циклы for также, как правило, наиболее эффективны, когда вы просто выполняете какую-либо сложную операцию, которая не строит список, например, для которых оптимизированы списки и карты (например,суммирование, создание дерева и т. д.) - по крайней мере, эффективно с точки зрения памяти (не обязательно с точки зрения времени, где я ожидаю в худшем случае постоянного фактора, за исключением некоторых редких патологических сбоев при сборке мусора).

«Пифонизм»

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

Лень

С точки зрения эффективности, как и большинство конструкций функционального программирования, КАРТА МОЖЕТ БЫТЬ ЛЕНИВОЙ, и на самом деле ленив в питоне.Это означает, что вы можете сделать это (в 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>)

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

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

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

Обобщить:

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

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

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

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

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

против:

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

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

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

Итак, начиная с 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

Существует также такая вещь, как выражение генератора, см. ПЭП-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()!

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

Я провел быстрый тест, сравнивающий три метода для вызова метода объекта.Разница во времени, в данном случае, незначительна и зависит от рассматриваемой функции (см. ответ).Здесь я рассмотрел следующие методы:

# 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 technique) является самым быстрым для обоих типов добавлений в объект, особенно с более короткими списками.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top