Рубріки: Теория

List Comprehensions (генераторы списков) в Python

Сергій Бондаренко

В Python присутствует синтаксическая конструкция, которая позволяет в одну строку заполнять списки простыми или сложными значениями. Называется она — генераторы списков или List Comprehensions. Сейчас мы поговорим об операциях с ними и расскажем о том, как их использовать в своих задачах.

Содержание:
1. Шаблон построения генератора списка
2. Коллекция из текста в генераторе списка
3. Работа с несколькими генераторами списков
4. Условные конструкции
5. Списки: ввод данных
6. Инициализация двухмерных списков
7. Вложенные циклы в генераторах списков
8. Функция map с генераторами списков
9. Проблемы с генераторами списков
Заключение

1. Шаблон построения генератора списка

Перевод термина List Comprehensions как «генераторы списков» не совсем удачен, однако мы будем использовать именно его, так как он ближе к пониманию искомой сути, чем, например, такие варианты перевода как «список понятий» или «абстракция списков».

Все генераторы списков строятся по одинаковому шаблону, который выглядит так:

[выражение for val in коллекция], где выражениевыражение, описывающее переменную, valпеременная.

На основе этого шаблона для примера напишем простой генератор списка:

a = [1 for i in range(8)]
print(a)

Вместо выражения мы просто запишем единицу. Так как в нашем цикле for стоит число восемь, мы восемь раз подставляем наше выражение — единицу. На экране появились восемь единиц — это наш список.

Указав вместо единицы значение двойки мы получим восемь двоек, а если изменим число циклов с восьми на десять — получим десять двоек и так далее.

Также мы можем указывать переменную, например, как i:

a = [i for i in range(8)]
print(a)

Так как в нашем случае переменная i будет принимать значение от нуля до семи, то эти a = [i%2 for i in range(8)]

print(a)

значения будут подставляться в наш список. Если записать генератор списка следующим образом, мы можем получить ряд квадратов чисел от нуля до семи:

a = [i**2 for i in range(8)]
print(a)

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

Обратите внимание — все то же самое мы можем сделать в цикле стандартными средствами Python.

Это может выглядеть, скажем, вот так:

N = 8
a = [0] * N
for i in range(N):
    a[i] = i ** 2
print(a)

Результат тот же самый [0, 1, 4, 9, 16, 25, 36, 49]. Однако вариант с генератором списка более удобен — одна строчка вместо нескольких. К тому же скорость работы в случае с List Comprehensions будет выше по сравнению с реализацией через цикл for.

Также можно задавать диапазон перебираемых значений. Например, следующее выражение выводит остатки чисел, деленных на четыре в диапазоне от 8 до 16:

a = [i%4 for i in range(8, 17)]
print(a)

[0, 1, 2, 3, 0, 1, 2, 3, 0]

2. Коллекция из текста в генераторе списка

В качестве коллекции мы можем обходить и другие итерабельные объекты, например, строки.

a = [i for i in 'HighloadToday']
print(a)

Так как мы внутри цикла перебираем все буквы нашей строки, на экране мы получим отдельные символы, то есть список будет заполнен символами строки:

['H', 'i', 'g', 'h', 'l', 'o', 'a', 'd', 'T', 'o', 'd', 'a', 'y']

Строки можно умножать. В результате мы получаем список, в котором символы продублированы заданное число раз.

Например, при умножении нашего примера на три мы имеем следующее:

a = [i*3 for i in 'HighloadToday']
print(a) 
['HHH', 'iii', 'ggg', 'hhh', 'lll', 'ooo', 'aaa', 'ddd', 'TTT', 'ooo', 'ddd', 'aaa', 'yyy']

Также внутри выражения мы можем использовать различные функции. Например, функция ord находит код таблицы символов ASCII

a = [ord(i) for i in 'HighloadToday']
print(a)

[72, 105, 103, 104, 108, 111, 97, 100, 84, 111, 100, 97, 121]

Для следующего примера мы возьмем число циклов десять раз, а в качестве функции — функцию случайных чисел randint, предварительно импортировав модуль random.

Пусть эта функция возвращает нам какое-то число из указанного диапазона.

import random
a = [random.randint(-10, 10) for i in range(10)]
print(a)

Результатом такого списка будут десять случайных чисел:

[-7, -1, 8, 9, -10, -7, 8, 5, 6, -7]

3. Работа с несколькими генераторами списков

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

import random
a = [random.randint(-10, 10) for i in range(10)]
print(a)
b = [abs(elements) for elements in a]
print(b)

[9, 7, -8, 10, 4, 9, -6, 0, 3, -1]
[9, 7, 8, 10, 4, 9, 6, 0, 3, 1]

В списке b остались значения только по модулю.

Генераторы списков позволяют менять исходные элементы списка. Так, следующая запись приведет к тому, что все элементы списка увеличатся на единицу:

import random
a = [random.randint(-10, 10) for i in range(10)]
print(a)
a = [elements+1 for elements in a]
print(a)

[4, -2, -5, 3, 3, -4, 3, 1, -6, -6]
[5, -1, -4, 4, 4, -3, 4, 2, -5, -5]

4. Условные конструкции

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

[выражение for val in коллекция if условие]

Предположим, мы составляем список b, в котором находятся только четные элементы. Добавим это условие в наш код. Каждый элемент генерируемого списка должен делиться на 2 с остатком 0:

import random

a = [random.randint(-10, 10) for i in range(10)]
print(a)
b = [elements for elements in a if elements % 2 == 0]
print(b)


[-10, -8, -3, 3, 8, -5, -7, -10, 3, 9]
[-10, -8, 8, -10]

Условие можно также сделать сложным, скажем, добавив новое требование — выводимые элементы должны быть меньше нуля и четными:

import random

a = [random.randint(-10, 10) for i in range(10)]
print(a)
b = [elements for elements in a if elements < 0 and elements % 2 == 0]
print(b)

[-6, 6, -4, -4, -6, 10, -5, -1, -10, -7]
[-6, -4, -4, -6, -10]

5. Списки: ввод данных

Предположим, нам необходимо ввести через пробел несколько чисел. Это делается несложной командой:

a = input()
print (a)

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

 a = input().split()
print(a)

11 7 4 1 111 22
['11', '7', '4', '1', '111', '22']

Теперь мы получили список, в котором можно преобразовать каждый элемент в целое число. Для этого задействуем функцию int(), которое преобразует значение к целому типу.

Далее мы будем использовать генератор списков по уже известному нам шаблону:

a = input().split()
a = [int(i) for i in a]
print(a)

Теперь наш список содержит числа:

111 22 5 15 7 6 41 14
[111, 22, 5, 15, 7, 6, 41, 14]

Данная задача также решается с помощью функции map, подробнее о которой речь пойдет ниже. Выглядеть это будет так:

s = list(map (int, input().split))
print (s)

6. Инициализация двухмерных списков

С помощью генераторов можно инициализировать двумерные списки. Предположим, у нас есть шесть строк и семь столбцов из элементов «единица».

n = 6
m = 7
a = [[1]*m for i in range(n)]
print[a] 

[[1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1]]

Мы получили массив, записанный в одну строку. Чтобы отобразить его более привычно, пройдемся циклом

for i in a:
    print(i)

И получим вывод:

Зададим какой-нибудь элемент этой матрицы. Пусть, например, в четвертой строке четвертый элемент равен восьми:

n = 6
m = 7
a = [[1]*m for i in range(n)]
a[3][3] = 8
for i in a:
    print(i)

И получим такой вывод:

7. Вложенные циклы в генераторах списков

Генераторы списков могут включать в себя двойные циклы. Предположим, у нас есть список [1, 2, 3] и мы обойдем им символы abc.

a = [(i, j) for i in 'abc' for j in [1, 2, 3]]
print(a)  

[('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3), ('c', 1), ('c', 2), ('c', 3)]

Как видим, у нас каждое значение списка сочеталось с каждым значением элемента abc.

Такие вложенные циклы генераторов можно сопровождать какими-то условиями. Например, возьмем произведение элементов списков и поставим условие отбора значений больше (или равное) десяти.

a = [(i * j) for i in [2, 3, 4, 5] for j in [1, 2, 3] if i * j >= 10]
print(a)

Мы получили три пары таких значений — [12, 10, 15].

8. Функция map с генераторами списков

Теперь поговорим о том, как можно использовать функцию map с генераторами списков. Функция map принимает функцию (func), а затем итерабильные последовательности — списки, кортежи, словари, строки и так далее.

class map (object)
map (func, *iterables)  - - > map object

Объект map представляет собой итератор, который вычисляет результат работы функции (func), которую мы передали на каждый аргумент из этой последовательности.

Допустим, мы имеем дело со списком a = [-1, 2, -3, 4, 5] и мы можем вызвать функцию map, в которой передадим функцию abs.

a = [-1, 2, -3, 4, 5]
b = map (abs, a)
print(b)

<map object at 0x0000018F3A739AF0>

Итератор представляет собой такую коллекцию, которую можно, во-первых, итерировать, то есть обходить все эти элементы коллекции в цикле for, а во-вторых, использовать функции, которые действуют над коллекциями.

Такие, как, например, «максимум», «минимум» и т.д. Функцию map оборачиваем функцией list, превращая переменную b в список.

a = [-1, 2, -3, 4, 5]
b = list (map (abs, a))
print(b)

К каждому элементу списка применилась функция abs.

[1, 2, 3, 4, 5]

Перепишем код этой программы, с помощью генератора списков.

a = [-1, 2, -3, 4, 5]
b = list(map(abs, a))
c = [abs(i) for i in a]
print(c)

Результат тот же самый [1, 2, 3, 4, 5].

В нашем примере мы передали встроенную функцию abs, однако с тем же успехом мы можем передавать функции, которые создадим сами при помощи инструкции def.

Создадим такую функцию:

def f(x):
    return x ** 2


a = [-1, 2, -3, 4, 5]
b = list(map(f, a))
print(b)

[1, 4, 9, 16, 25]

Функция map будет брать значения из нашего списка a и передавать их в функцию f в качестве локальной переменной x, после чего вернет квадрат этого значения.

Занесем в список a строки, чтобы рассмотреть еще один пример. Для строк вы тоже можете вызывать встроенные функции, которые они поддерживают. Одной из таких функций является len:

a = ['Highload', 'Today']
b = list(map(len, a))
print(b)

В консоли отображаются длины слов «Highload» и «Today»:

[8, 5]

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

Чтобы его вызвать, мы должны прописать встроенный объект str и далее название его метода:

a = ['Highload', 'Today']
b = list(map(str.upper, a))
print(b)

В результате мы имеем список из слов, написанных заглавными буквами:

['HIGHLOAD', 'TODAY']

Функцию map можно также передавать анонимной функции. Они создаются при помощи инструкции lambda. Причем здесь мы должны указать, что наша функция должна принимать один аргумент, в который будут поочередно присваиваться строки нашего списка а.

Мы берем его срез от начала до конца с шагом минус один. Тем самым мы получаем строку наоборот:

a = ['Highload', 'Today']
b = list(map(lambda x: x[::-1], a))
print(b)

['daolhgiH', 'yadoT']

Все, что мы делаем при помощи функции map, мы точно также можем делать и с помощью генератора списка. Для этого в квадратных скобках обходим все элементы нашего списка a и устанавливаем срез с шагом минус один.

a = ['Highload', 'Today']
b = list(map(lambda x: x[::-1], a))
c = [i[::-1]for i in a]
print(c)

['daolhgiH', 'yadoT']

9. Проблемы с генераторами списков

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

Если длина такого ряда составляет сто чисел, тысячу или, даже, миллион — компьютер справится с задачей. Однако если количество суммируемых элементов слишком велико, скажем, миллиард, компьютер может не «переварить» такую задачу.

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

a = sum([i * i for i in range(1000000)])
print(a)

В этом случае имеет смысл воспользоваться иными инструментами Python для реализации данной задачи. Например, можно использовать генератор, который не помещает весь список в память, а в каждый отдельный момент удерживает только одно значение — то, которое он возвращает.

Заключение

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

Для закрепления материала рекомендуем вам посмотреть видео, где подробно объяснена данная тема:

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

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

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023