Строку из чисел в список чисел на Python
Совсем недавно столкнулся с задачкой – надо было оптимизировать код программы.
Представим себе совершенно стандартную ситуацию: с помощью input() мы получаем строку, состоящую из чисел, записанных через пробел:
0 1 2 3 4 5 6 7 8 9 10
Числа могут быть разные – от маленьких до совсем больших. Из такой строки нужно сделать список, содержащий в себе числа.
У меня было несколько вариантов решения, которыми я бы и хотел поделиться. Внизу много теории, поэтому статья может быть интересна в основном новичкам в Python.
###Подготовка
Все описанные действия проводились на Python версии 3.4, а полностью код можно посмотреть тут.
Надо учитывать, что строка, которую нам придется обрабатывать, может быть очень длинной, например, содержать в себе 1000 чисел Фибоначчи:
Полученную строку мы и будем использовать в дальнейших примерах.
###Варианты
Самый первый, который приходит на ум – это с использованием map().
Данная встроенная функция основана на применении итерационного протокола. Она представляет собой инструмент, вызывающий заданную функцию для каждого элемента итерируемого объекта, а возвращает итерируемый объект, вместо того, чтобы воспроизводить сразу весь список с результатами. По нему можно пройтись, например, элементарным for, но это получится только один раз, для последующего прохода нужно будет снова создавать итератор. Проще говоря, поведение map() ориентировано на экономию существенных объемов оперативной памяти, что нам и пригодится.
Данная функция возвращает объект-генератор, который мы смело скормим list(). Что нам может здесь не понравится, так это fib_str.split()
, создастся новый список из строк, забьется память.
Вспомним, что для контейнерных объектов, таких как списки, кортежи и словари, sys.getsizeof() возвращает размер памяти в байтах, занимаемый самим контейнером, а не суммарный объем памяти, занимаемой всеми элементами контейнера. Именно поэтому мы использовали sum().
В следующем варианте – используем выражение-генератор:
Функция возвращает объект-генератор, который в свою очередь поддерживает протокол итераций, в течение которых и будет воспроизводить значения. В памяти не окажется списка с результатами, разве что отличится split(), но это уже наша вина.
В третьем варианте мы будем использовать генератор списков:
Генератор списков – это способ построить новый список, применяя выражение к каждому элементу последовательности. Более того, генераторы списков могут выполняться значительно быстрее, чем инструкции циклов for, потому что итерации выполняются со скоростью языка C, а не со скоростью программного кода на языке Python. В отличие от выражений-генераторов, генераторы списков создают списки, содержащие результаты вычислений, а не воспроизводят данные по требованию.
В итоге к нам придет готовый список, а это значит, что память забьется:
Последний вариант – все очень хорошо, если бы split() создавал объект-генератор, но он этого не делает, поэтому напишем свою функцию-генератор:
Единственное их отличие от обычных функций – они автоматически поддерживают протокол итераций и могут использоваться в контексте итераций (yield вместо return).
###Сравнение
Теперь настало время выяснить, какой способ работает быстрее. Еще раз внимательно изучите код, попробуйте сами ответить на этот вопрос.
Итак, сейчас посмотрим на время создания объекта-генератора:
Проще говоря, inum() - 0.558 µs, mk() - 367 µs, а fk() - 370 µs. Понятное дело, драгоценное время тратит split(), от которого мы избавились в inum(). gk() сразу создаст список, поэтому сейчас он нас не интересует.
Наконец, создадим список, состоящий из целых чисел:
Результаты:
Вдруг inum() показывает совершенно другой результат. Поздравляю, если вы в самом начале догадались, почему, а если нет, то посмотрим на общее число вызовов, общее время, потраченное на выполнение функций. Запустим cProfile:
Как мы видим, inum() вызывается столько же раз, сколько и return в fk(), только выполняется значительно дольше, чего нельзя сказать о mk().
###Вывод
Что по этому поводу пишет нам Лутц:
Современная реализация map() выполняется быстрее, чем любой цикл for, и обладает более высокой производительностью, чем генераторы списков. Выражения-генераторы в первую очередь оптимизируют использование памяти – они не требуют создания в памяти полного списка с результатами. Кроме того, на практике они могут работать несколько медленнее, поэтому их лучше использовать, только когда объем результатов очень велик.
Однако, Бизли так не считает:
Вместо функции map() практически всегда лучше использовать выражения-генераторы или генераторы списков (которые обладают более высокой производительностью).
Прежде чем использовать тот или иной вариант, проведите хронометраж кода на своем компьютере, со своей версией Python. В моем случае выиграл достаточно лаконичный и простой вариант с map().
###Список используемой литературы
- Марк Лутц “Изучаем Python”, стр. 257, 426, 429, 435, 555-556, 568, 573-574, 590
- Дэвид Бизли “Python. Подробный справочник”, стр. 150, 250, 267