Привет, Хабр. Часто при работе с последовательностями встает вопрос об их создании. Вроде бы привык использовать списковые включения (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.: Если я что-то упустил, то с радостью готов обсудить это вместе с вами в комментариях.

Комментарии (25)


  1. AMDmi3
    08.12.2019 22:46
    +3

    Упустили как минимум generator expressions и обычные генераторы (с yield).


  1. Magikan
    09.12.2019 00:36
    +1

    как вообще можно сравнивать list comprehension и около генераторное выражение обернутое в лист? или сравнивайте корректно строго на 2.7 или вместо list comprehension используйте генератор обернутый в лист. а то получается попытка сравнить несравнимое и впихнуть невпихуемое.


  1. lair
    09.12.2019 00:52
    +1

    Если Вам не требуются все вычисленные значения сразу (а может и вообще не потребуются), то Вам стоит остановить свой выбор на map.

    Непонятно, почему вы сравниваете с list expressions, а не с generator expressions.


  1. vinograd19
    09.12.2019 01:07

    Питонячий dis даёт разницу в инструкции CALL_FUNCTION. Она отсутсвует в вашем примере

    [x*x for x in values]
    . Добавьте её туда для более корректного сравнения

    $ python -m timeit -r 10 -s "a = lambda x: x*x" "list(map(a, range(1000000)))" 
    $ python -m timeit -r 10 -s "a = lambda x: x*x" "[a(x) for x in range(1000000)]" 
    


    List comprehension удобнее тем, что фильтрацию удобно вставлять
    
    [a(x) for x in range(1000000) if x%2 == 0]
    list(map(a, filter(lambda x: x%2 == 0, range(1000000))))
    


    Также generator-comprehension generator-expression можно делать подобным образом.
    
    (x*x for x in range(10))
    


    1. DaneSoul
      09.12.2019 01:15

      Также generator-comprehension можно делать подобным образом.
      (x*x for x in range(10))
      Терминологически несколько неоднозначно его обозвали.
      То что вы написали generator expression, a не comprehension. Разница принципиальная, те генераторы которые comprehensions (list, set, dictionary) пишутся в квадратных скобках и выдают всю коллекцию, а те которые generator expression (как в вашем примере) — по одному элементу при каждом обращении.


      1. vinograd19
        09.12.2019 01:19

        Да. «generator comprehension» — это просто путаница. Есть только
        Generator expressions and list comprehensions.


  1. DaneSoul
    09.12.2019 01:11
    +1

    1. vasily-v-ryabov
      09.12.2019 07:15

      Спасибо! Как раз хотел сказать, что list comprehension читабельнее и проще для понимания на мой личный вкус. Поэтому я почти всегда за него. В статье по ссылке это тоже есть.


  1. worldmind
    09.12.2019 09:44

    Питон не тот язык где уместны такие «оптимизации», питон для тех случаев когда читаемостью кода жертвовать нельзя, именно поэтому списковые включения нужно использовать всегда и это официальная рекомендация, что кстати намекает что автор что-то неправильно замерял — уверен что списковые включения как минимум не медленнее чем map.


    1. Magikan
      09.12.2019 10:01

      если учитывать, что map в 3м питоне превратился в генератор, то разница между ними все таки ощутимая. асимтоматика перебора значений так и останется линейной в обоих случаях, а вот по памяти и скорости инициализации любой генератор утрет нос list comprehension'y.


      1. worldmind
        09.12.2019 10:07

        Гуидо не согласен:

        In addition, the list comprehension executes much faster than the solution using map and lambda. This is because calling a lambda function creates a new stack frame while the expression in the list comprehension is evaluated without creating a new stack frame.


        1. Magikan
          09.12.2019 10:23

          с этим я спорить не собираюсь, в условиях что мы хотим сделать что-то простое как x+1. однако это не отменяет сказанного мной: генератор иниализируется быстрее полного построения готового списка, памяти требует примерно ноль, асимтоматика перебора значений линейная в обоих случаях. однако, если проводить честные замеры, то в list comprehension мы обязаны вызывать какую-нибудь функцию, не потому, что так надо, а банально для чистоты эксперимента тк вызов функции одна из самых дорогих операций и просто взять ее, выкинуть и сказать что генераторы отстой — это как сказать что запорожец с реактивным двигателем быстрее болида формулы 1.


          1. worldmind
            09.12.2019 10:29

            Так это о другом — конечно не надо строить весь список если его можно не строить, например не нужен доступ по индексу, тогда можно юзать generator expression зачем map?


            1. Magikan
              09.12.2019 10:35

              предлагаю закрыть тему тк дело не в том как я или Вы смотрите на эти 2 сильно разные вещи, а в том, о чем в статье не сказано при проведении сравнений и замеров: чем они отличаются и какие плюсы и минусы у них есть. а самое главное не сказано: выбирайте инструмент исходя из задачи )


      1. worldmind
        09.12.2019 10:10

        Но если без лямбды то какой-то мизер можно получить:

        map may be microscopically faster in some cases (when you're NOT making a lambda for the purpose, but using the same function in map and a listcomp).


      1. vinograd19
        09.12.2019 10:46

        map — это не совсем генератор. У него нет, например, метода send.


  1. aamonster
    09.12.2019 10:13
    +2

    Это питон. Тут надо биться не за производительность*, а за простоту и понятность кода.


    • Разумеется, не охреневая до O(n?) вместо O(n) или сжирания кучи памяти на ровном месте.


  1. Tiendil
    09.12.2019 10:29

    Если мне память не изменяет, то разработчики питона давно рекомендуют использовать list comprehensions вместо map, filter и прочего. По крайней мере при мирграции на третий питон утилитка 2to3 старалась заменять map & filter на них.


  1. zanac
    09.12.2019 10:37

    Автор ниасилил мануал. Даже статью написал, хы.
    помню костылял сверхбыстрый файлменеджер для старой нокиии на симбе, было весело. В результате — список из 800+ файлов и папок, причем сперва папки, все в алфавитном порядке и за 0.2 секунды.


  1. YuriM1983
    09.12.2019 13:59

    С приходом Python 3.8 и PEP 572, пользы от генераторов и list/set/dict comprehension стало куда больше, нежели от map/filter.


  1. sergey-gornostaev
    09.12.2019 17:07

    Даже интересно стало, что за книжки такие, которые кричат об обязательности использования map? Гвидо ван Россум называет map ошибкой и призывает использовать вместо него включения. Дэвид Бизли недавно написал «I can't even remember the last time I used map. 1999? It was a long time ago to be sure.»


  1. kai3341
    09.12.2019 18:59

    Статья в духе "я продолжаю начинать изучать программирование". Автор копнул глубоко — в дизассемблер байткода, и в то же время безумно поверхностно:


    1. Не рассмотрены generator expressions
    2. Выбор между map и list/generator expression определяется в первую очередь сложностью обработки каждого элемента. Одно дело, если каждый элемент надо возвести в квадрат, и совсем другое дело, если для каждого элемента нужно сходить в базу, потом в сторонний сервис, а ещё это залогировать


    1. somurzakov
      10.12.2019 01:45

      ваш п.2 порождает классические O(N^2) алгоритмы, когда на каждый айтем в списке нужно заново делать монструозные действия по одному, связанные с вводом-выводом и задержкой


      1. fireSparrow
        10.12.2019 12:37
        +1

        Откуда здесь O(N^2)?
        Чтоб вы знали, нотация O(N^2) имеет вполне конкретный смысл, она не обозначает просто «очень тяжёлые вычисления».


        1. kai3341
          10.12.2019 17:35

          В детстве меня пугали бабайкой. Сейчас пугают O(N^2). Интересно, что не было ни бабайки, ни O(N^2)