[Из песочницы] List Comprehension vs Map

Привет, Хабр. Часто при работе с последовательностями встает вопрос об их создании. Вроде бы привык использовать списковые включения (List Comprehension), а в книжках кричат об обязательном использовании встроенной функции map.

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

image


List Comprehension


Списковое включение — это встроенный в Python механизм генерации списков. У него только одна задача — это построить список. Списковое включение строит список из любого итерируемого типа, преобразуя (фильтруя) поступаемые значения.

Пример простого спискового включения для генерации списка квадратов чисел от 0 до 9:

squares = [x*x for x in range(10)]


Результат:

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]


map


map — встроенная в язык функция. Принимает на вход в качестве первого параметра — функцию, а в качестве второго — итерируемый объект. Возвращает генератор (Python 3.x) или список (Python 2.x). Я остановлю свой выбор на Python 3.

Пример вызова функции map для генерации списка квадратов чисел от 0 до 9:

squares = list(map(lambda x: x*x, range(10)))


Результат:

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]


Сравнение производительности


Построение без функций


В качестве эксперимента будем считать квадраты чисел из промежутка от 0 до 9,999,999:

python -m timeit -r 10 "[x*x for x in range(10000000)]"
python -m timeit -r 10 "list(map(lambda x: x*x, range(10000000)))"


Результаты:

1 loop, best of 10: 833 msec per loop
1 loop, best of 10: 1.22 sec per loop


Как видим способ с List Comprehension работает примерно на 32% быстрее. Продизассемблировав не удается получить полных ответов, так как функция map «как будто скрывает детали своей работы». Но скорее всего это связано с постоянным вызовом lambda функции, внутри которой уже делаются вычисления квадрата. В случае с List Comprehension нам требуются только вычисления квадрата.

Построение с функциями


Для сравнения будем также считать квадраты чисел, но вычисления теперь будут находиться внутри функции:

python -m timeit -r 10 -s "def pow2(x): return x*x" "[pow2(x) for x in range(10000000)]"
python -m timeit -r 10 -s "def pow2(x): return x*x" "list(map(pow2, range(10000000)))"


Результаты:

1 loop, best of 10: 1.41 sec per loop
1 loop, best of 10: 1.21 sec per loop


В этот раз ситуация обратная. Метод с использованием map оказался на 14% быстрее. В данном примере оба метода оказываются в одинаковой ситуации. Оба должны вызывать функцию для вычисления квадрата. Однако внутренние оптимизации функции map позволяют ей показывать лучшие результаты.

Что же выбрать?


Ниже представлено правило выбора нужного способа:

image

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

map «безопаснее»?


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

Например:

symbols = ['a', 'b', 'c']
values = [1, 2, 3]
for x in symbols:
  print(x)
  squared = [x*x for x in values] # Все плохо. "x" из верхнего "for" затёрт
  print(x)


Вывод программы будет следующим:

a
3
b
3
c
3


Теперь перепишем этот же код с использованием map:

symbols = ['a', 'b', 'c']
values = [1, 2, 3]
for x in symbols:
  print(x)
  squared = map(lambda x: x*x, values) # Все норм, это локальная область видимости
  print(x)


Вывод:

a
a
b
b
c
c


Самые наблюдательные уже могли заметить по синтаксису использования map, что это Python 2. Действительно, во втором питоне была подобного рода проблема с затиранием переменных. Однако в Python 3 эта проблема была исправлена и больше не является актуальной.

Описанные выше примеры покажут одинаковые результаты. Может также показаться, что это глупая ошибка и ты такую никогда не допустишь, однако это может произойти когда вы просто перенесли блок кода с внутренним циклом из другого блока. Такая ошибка может потратить у Вас кучу времени и нервов на её исправление.

Заключение


Сравнение показало, что каждый из способов хорош в своей ситуации.

  • Если Вам не требуются все вычисленные значения сразу (а может и вообще не потребуются), то Вам стоит остановить свой выбор на map. Так по мере надобности Вы будете запрашивать порцию данных у генератора, экономя при этом большое количество памяти (Python 3. В Python 2 это не имеет смысла, так как map возвращает список).
  • Если Вам нужно вычислить сразу все значения и вычисления можно сделать без использования функций, то выбор Вам стоит сделать в сторону List Comprehension. Как показали результаты экспериментов — он имеет существенное преимущество в производительности.


P.S.: Если я что-то упустил, то с радостью готов обсудить это вместе с вами в комментариях.

© Habrahabr.ru