Python для сбора и анализа данных

Алла Тамбовцева, НИУ ВШЭ

Генераторы списков (списковые включения)

В Python для создания новых списков на основе старых существуют удобные конструкции, которые называются генераторы списков или списковые включения (list comprehensions). Они позволяют написать код, более компактный и быстрый по сравнению с кодом с использованием цикла for и метода .append(). Вспомним, как мы создавали новый список на основе старого с помощью цикла.

Пусть у нас есть список целых чисел nums:

In [1]:
nums = [1, 8, 23, 45, 67]

Создадим теперь пустой список nums_sq и заполним его квадратами чисел из nums:

In [2]:
nums_sq = [] 
for n in nums:
    nums_sq.append(n ** 2)
print(nums_sq) 
[1, 64, 529, 2025, 4489]

Теперь рассмотрим решение той же задачи, но с помощью генераторов списков:

In [3]:
nums_sq = [n ** 2 for n in nums] 
print(nums_sq) 
[1, 64, 529, 2025, 4489]

Как устроен генератор списка? Во-первых, код для преобразований элементов старого списка записывается в квадратных скобках, так как мы создаем новый список. Во-вторых, мы пишем тот же код, что и в случае с циклом, но «раскручиваем» его с конца: сначала указываем, что за операцию надо выполнить, а потом – для каких элементов ее повторить.

Теперь давайте проверим, что код с генератором списка работает быстрее. В начале ячейки (это обязательно должна быть первая строка, если первой строкой будет идти что-то еще, даже комментарий, ничего не сработает) напишем «магическую строку %%timeit. «Магическая строка» – это официальное название, так называются строки кода в Jupyter, которые начинаются с %% и отвечают за режим исполнения ячейки в Jupyter Notebook. В данном случае команда timeit отвечает за измерение времени исполнения кода.

Для примера возьмем какой-нибудь список побольше – создадим список из кубов целых чисел от 0 до 5000 включительно на основе range(). Сначала сделаем это с помощью цикла и .append():

In [4]:
%%timeit
R = []
for i in range(0, 5001):
    R.append(i ** 3) 
1.47 ms ± 8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Выдача сообщает нам, что ячейка с кодом выше была запущена 7 раз по 1000 раз, и что среднее время выполнения кода за такое число прогонов равно 1.47 милисекундам, а стандартное отклонение равно 8 микросекундам (на каждой системе в разное время будут свои числа). Почему недостаточно прогнать код один раз? Потому что хочется получить более общие результаты, с учетом разных факторов. Каждую секунду на компьютере выполняется множество процессов, которые мы явно не видим, но которые влияют на время исполнения кода. Поэтому, запуская ячейку много раз, Jupyter пытается оценить скорость выполнения кода в разные моменты времени и вывести сводные характеристики результатов.

Теперь проделаем то же самое, но для генератора списка:

In [5]:
%%timeit
R = [i ** 3 for i in range(0, 5001)] 
1.29 ms ± 5.58 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Хотя результаты у всех получаются разные, в любом случае нетрудно заметить, что исполнение кода с генератором списка заняло меньше времени, чем исполнение кода с циклом.

Функция enumerate()

Прежде, чем разбираться с функцией-счетчиком enumerate(), давайте рассмотрим такую задачу. У нас есть список напитков drinks, и мы хотим выводить на экран номер (индекс) напитка и название напитка.

In [6]:
drinks = ['tea', 'coffee', 'juice', 'milk'] 

Если решать задачу рационально, используя готовые методы на списках Python, решение будет выглядеть так:

In [7]:
for d in drinks:
    print(drinks.index(d), d)  # метод index()
0 tea
1 coffee
2 juice
3 milk

Если бы мы забыли про существование метода .index(), нам пришлось бы создавать перечень индексов списка drinks самостоятельно, например, с помощью range(), а потом делать перебор по этим индексам в цикле:

In [8]:
for i in range(0, len(drinks)):
    print(i, drinks[i]) 
0 tea
1 coffee
2 juice
3 milk

Код выше технически верный, но не-питоновский и не очень рациональный.

Теперь посмотрим на решение той же задачи с помощью функции enumerate(). Эта функция – счетчик, который позволяет нумеровать элементы в списке. Если мы попробуем применить эту функцию к списку drinks и посмотреть, что за результат нам вернется, мы увидим что-то странное:

In [9]:
enumerate(drinks)
Out[9]:
<enumerate at 0x10dc97a50>

Python сообщает нам, что это объект типа enumerate, он от нас скрыт, но хранится в ячейке памяти 0x10dc97a50. Чтобы все же увидеть содержимое, можем преобразовать результат в список:

In [10]:
list(enumerate(drinks))  # list of tuple
Out[10]:
[(0, 'tea'), (1, 'coffee'), (2, 'juice'), (3, 'milk')]

Это список кортежей (tuples, о них поговорим позже), список пар, где на первом месте стоит индекс элемента, а на втором – сам элемент. Теперь мы сможем запустить цикл по такому списку:

In [11]:
for i, d in enumerate(drinks):
    print(i, d) 
0 tea
1 coffee
2 juice
3 milk

После оператора for можно указывать несколько переменных, причем называться они могут, как угодно. Python в любом случае поймет, что i – это первый элемент в паре в списке, возвращаемом enumerate(), а d – это второй элемент.