Списковые включения в Python: легко ли это понять?!?

Вы когда-нибудь слышали о list comprehension в Python? Он упрощает работу со списками и делает ваш код более лаконичным.

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

Сначала мы дадим определение list comprehension, а затем рассмотрим ряд примеров, которые сделают это частью ваших знаний в области программирования.

Давайте вместе откроем для себя list comprehension!

Что делает понимание списка?

Списковые генераторы Python позволяют создавать совершенно новые списки или генерировать списки путем фильтрации или сопоставления существующих списков.

Списковые включения используют следующий синтаксис:

new_list = [expression(item) for item in iterable if condition]

Примерами итерируемых объектов в Python являются списки, кортежи, множества и строки.

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

Также можно указать необязательное условие для фильтрации элементов в итерируемом объекте.

Результатом работы спискового генератора является новый список, для создания которого потребовалось бы гораздо больше строк кода, если бы его пришлось создавать с использованием стандартных циклов for и операторов if.

Вот как будет выглядеть приведенный выше однострочный код без list comprehension:

new_list = []

for item in iterable:
    if condition:
        new_list.append(expression(item))

Гораздо лучше в одной строке!

Списковое включение так называется, потому что это всеобъемлющий или полный способ описания последовательности в Python.

Как создать новый список с помощью List Comprehension

Одной из основных операций, которую вы можете выполнить с помощью спискового, является создание нового списка.

Например, давайте посмотрим, как создать новый список, используя функцию range внутри генератора списков.

>>> numbers = [x for x in range(10)]
>>> print(numbers)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Выражение list comprehension в этом случае очень простое, это просто x.

Давайте обновим выражение, удвоив значение x:

>>> numbers = [2*x for x in range(10)]
>>> print(numbers)
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Как видите, мы создали список, в котором каждый элемент умножен на 2.

Выражение может быть любым.

Как добавить один условный оператор в list comprehension

Начнем с понимания списка в предыдущем разделе.

Следующий шаг на пути к более глубокому изучению list comprehension — добавление к ним условия.

Синтаксис, который мы будем использовать, следующий:

new_list = [expression(item) for item in iterable if condition]

Предположим, например, что мы все еще хотим создать следующий список:

>>> numbers = [2*x for x in range(10)]

Но на этот раз мы хотим исключить числа больше или равные 5.

>>> numbers = [2*x for x in range(10) if x < 5]
>>> print(numbers)
[0, 2, 4, 6, 8]

Мы отфильтровали элементы в новом списке с помощью условия.

Как добавить два условия в list comprehension

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

Обновите предыдущее list comprehension, чтобы учитывать только числа от 2 до 5 (2 и 5 исключены).

>>> numbers = [2*x for x in range(10) if x > 2 and x < 5]
>>> print(numbers)
[6, 8]

Имеет ли это смысл?

Как преобразовать цикл for в списковое включение?

Начнем с определения списка строк:

animals = ['tiger', 'lion', 'elephant']

Я хочу создать цикл for, который добавляет букву «s» в конец каждой строки, чтобы создать список форм множественного числа.

>>> new_animals = []
>>> for animal in animals:
...     new_animals.append(animal+'s')
... 
>>> print(new_animals)
['tigers', 'lions', 'elephants']

Сначала мы определяем новый пустой список, который будем использовать для множественного числа существительных. Затем на каждой итерации цикла for мы используем метод append для добавления строки в новый список.

Этот код работает, но есть ли способ сделать его более лаконичным?

Используя списочное представление, мы можем упростить этот код, вот как:

>>> new_animals = [animal+'s' for animal in animals]
>>> print(new_animals)
['tigers', 'lions', 'elephants']

Замечательно!

С помощью одной строки кода мы создали новый список вместо использования трех строк кода, как мы видели ранее.

При использовании спискового генератора нам не нужно создавать пустой список в начале.

В этом представлении list comprehension представляет собой animal+'s', за которым следует цикл for, который по одному проходит по элементам исходного списка и применяет выражение к каждому из них.

Можно ли использовать else в list comprehension?

В предыдущем примере мы использовали оператор if в list comprehension.

Но можно ли также использовать оператор else для добавления нескольких условий в списочное включение?

Давайте попробуем…

…начнем с кода ниже:

>>> numbers = [2*x for x in range(10) if x > 2 and x < 5]

Чтобы добавить условие else, нам нужно изменить порядок элементов списка.

Нам нужно переместить условие перед ключевым словом for, чтобы мы могли вернуть значение, отличное от 2*x, если условие if не выполняется.

>>> numbers = [2*x if x > 2 and x < 5 else 3*x for x in range(10)]
>>> print(numbers)
[0, 3, 6, 6, 8, 15, 18, 21, 24, 27]

Итак, вот что происходит в этом коде…

Если значение x находится в диапазоне от 2 до 5, функция генерации списка возвращает 2*x, в противном случае она возвращает 3*x.

Например, число 1 не находится между 2 и 5, поэтому результат будет 3*1 = 3.

Использование elif в list comprehension

Невозможно использовать оператор elif в list comprehension, но можно реализовать то же поведение, используя несколько операторов else.

Начните со следующего кода:

>>> numbers = [2*x if x > 2 and x < 5 else 3*x for x in range(10)]

На данный момент состояние следующее:

  • если x > 2 и x < 5 => вернуть 2*x
  • иначе => вернуть 3*x

Я хочу реализовать следующее поведение:

  • если x > 2 и x < 5 => вернуть 2*x
  • иначе если x <=2 => вернуть 3*x
  • иначе => вернуть 4*x

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

>>> numbers = [2*x if x > 2 and x < 5 else 3*x if x <=2 else 4*x for x in range(10)]
>>> print(numbers)
[0, 3, 6, 6, 8, 20, 24, 28, 32, 36]

Я знаю, это длинное выражение, и на данном этапе я бы рассмотрел возможность использования стандартной реализации вместо list comprehension.

Написание работающего кода — это не единственное, что имеет значение…

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

Это представление списка можно также записать следующим образом (все еще без использования elif):

numbers = []

for x in range(10):
    if x > 2 and x < 5:
        numbers.append(2*x)
    else:
        if x <=2:
            numbers.append(3*x)
        else:
            numbers.append(4*x)  

Если вывести значение чисел, то получите тот же результат:

[0, 3, 6, 6, 8, 20, 24, 28, 32, 36]

Этот код определенно более читабелен, чем генератор списка, и он может стать еще более читабельным, если мы используем оператор elif:

numbers = []

for x in range(10):
    if x > 2 and x < 5:
        numbers.append(2*x)
    elif x <=2:
        numbers.append(3*x)
    else:
        numbers.append(4*x)

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

Как использовать оператор break в list comprehension

В стандартном цикле for в Python вы можете остановить выполнение цикла с помощью оператора break, если возникает определенное условие.

Как можно сделать то же самое с list comprehension?

Списковые включения не поддерживают оператор break, но можно использовать альтернативные подходы для имитации поведения оператора break.

Например, предположим, что у нас есть список случайных чисел, и мы хотим остановить выполнение функции list comprehension, если встретится определенное число.

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

import random

random_numbers = []
while len(random_numbers) < 10:
    random_number = random.randint(1, 5)
    random_numbers.append(random_number)

Мы создаем пустой список, а затем добавляем в него случайные числа от 1 до 5, пока список чисел не будет содержать 10 элементов.

Вот что получилось:

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

Теперь давайте добавим оператор break, чтобы остановить выполнение цикла while, если встретится число 3.

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

import random

random_numbers = []
while len(random_numbers) < 10:
    random_number = random.randint(1, 5)
    random_numbers.append(random_number)

    if random_number == 3:
        break

Программа работает отлично. Возможно, вам придется запустить ее несколько раз, если число 3 не генерируется random.randint.

[5, 3]

Теперь давайте сделаем то же самое с помощью функции «Список», начнем с генерации полного списка из 10 случайных чисел…

>>> random_numbers = [random.randint(1,5) for x in range(10)]
>>> print(random_numbers)
[2, 2, 4, 4, 4, 1, 3, 5, 2, 4]

И снова понимание списка рулит! Одна строка заменяет несколько строк кода.

И как теперь остановить понимание списка, если встретится число 3?

Один из возможных подходов требует внешнего модуля: itertools. Мы будем использовать функцию itertools.takewhile().

Сначала мы генерируем список random_numbers.

>>> import itertools
>>> random_numbers = [random.randint(1,5) for x in range(10)]
>>> print(random_numbers)
[2, 3, 5, 4, 5, 4, 2, 5, 3, 4]

Затем мы передаем его функции itertools.takewhile.

>>> print(itertools.takewhile(lambda number: number!=3, random_numbers))
<itertools.takewhile object at 0x7f88a81fe640>

Функция itertools.takewhile принимает следующие значения:

  • первый аргумент — лямбда, определяющая условие продолжения работы программы.
  • второй аргумент — итерируемый.

Он возвращает объект itertools.takewhile, который нам нужно преобразовать в список, чтобы увидеть элементы.

>>> print(list(itertools.takewhile(lambda number: number!=3, random_numbers)))
[2]

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

Это происходит потому, что мы сначала генерируем полный список случайных чисел, а затем просматриваем их, пока не встретим число 3.

Также во второй реализации число 3 не включено в окончательный список.

В этом сценарии определенно намного проще использовать оператор break, чем запутанное представление списка, требующее itertools.takewhile и лямбда-выражение.

Это немного слишком! 😀

Используйте понимание списка с двумя или более списками

Одним из способов применения списочного включения к двум или более спискам является его использование вместе с функцией zip().

>>> cities = ['Rome', 'Warsaw', 'London']
>>> countries = ['Italy', 'Poland', 'United Kingdom']
>>> [(city, country) for city, country in zip(cities, countries)]
[('Rome', 'Italy'), ('Warsaw', 'Poland'), ('London', 'United Kingdom')]

Генератор списков, используемый с функцией zip, возвращает список кортежей, где n-й кортеж содержит n-й элемент каждого списка.

То же самое происходит, если мы передаем три списка в функцию представления списков (и так далее).

>>> cities = ['Rome', 'Warsaw', 'London']
>>> countries = ['Italy', 'Poland', 'United Kingdom']
>>> languages = ['Italian', 'Polish', 'English']
>>> [(city, country, language) for city, country, language in zip(cities, countries, languages)]
[('Rome', 'Italy', 'Italian'), ('Warsaw', 'Poland', 'Polish'), ('London', 'United Kingdom', 'English')]

Заменить map и lambda на List Comprehension

Функция map применяет заданную функцию к элементам итерируемого объекта.

Например, вы можете использовать функцию map, чтобы удвоить значение каждого числа в списке.

>>> numbers = [3, 6, 8, 23]
>>> print(map(lambda x: 2*x, numbers))
<map object at 0x7f88a820d2e0>
>>> print(list(map(lambda x: 2*x, numbers)))
[6, 12, 16, 46]

Обратите внимание, что первый аргумент, переданный функции map, — это лямбда-функция.

А вот как можно записать это выражение, используя списочное выражение.

>>> [2*x for x in numbers]
[6, 12, 16, 46]

Очень просто!

Используйте списковое включение вместо фильтров и лямбда-функций

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

Например, давайте отфильтруем из предыдущего списка чисел те, которые меньше 10.

Условие передается в качестве первого аргумента функции фильтра и выражается в виде лямбда-функции.

>>> print(filter(lambda x: x<10, numbers))
<filter object at 0x7f88a8202340>
>>> print(list(filter(lambda x: x<10, numbers)))
[3, 6, 8]

А теперь напишите ту же логику, используя list comprehension.

>>> [x for x in numbers if x < 10]
[3, 6, 8]

Заменить Reduce и Lambda на List Comprehension

Функция reduce, примененная к нашему списку чисел, возвращает общую сумму, основанную на том факте, что мы используем следующую лямбда-функцию:

lambda a,b: a+b

Вот результат вызова функции reduce:

>>> from functools import reduce
>>> numbers = [3, 6, 8, 23]
>>> print(reduce(lambda a,b: a+b, numbers))
40

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

>>> print(sum([number for number in numbers]))
40

Как использовать вложенные списочные включения

Вложенные списочные генераторы могут быть полезны при работе со list comprehension.

Например, предположим, что мы хотим написать код, который увеличивает каждое число в матрице на единицу.

Это наша исходная матрица:

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Используя циклы for, мы бы сделали следующее:

for row in matrix:
    for index in range(len(row)):
        row[index] += 1

Обновленная матрица выглядит следующим образом:

[[2, 3, 4], [5, 6, 7], [8, 9, 10]]

Как можно использовать списковое включение вместо двух вложенных циклов?

Мы могли бы попробовать просто перевести приведенный выше код в списочное представление.

>>> [[row[index]+1 for index in range(len(row))] for row in matrix]
[[2, 3, 4], [5, 6, 7], [8, 9, 10]]

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

Разница между пониманием списка и выражением генератора

Конструкция Python, которая очень похожа на list comprehension, — это выражение-генератор.

Чтобы преобразовать list comprehension в выражение генератора, замените квадратные скобки круглыми.

Давайте посмотрим, как это можно применить к списку случайных чисел, который мы использовали ранее.

Не забудьте импортировать модуль random перед запуском следующего кода, в противном случае вы увидите исключение NameError.

Понимание списка

>>> random_numbers = [random.randint(1,5) for x in range(10)]
>>> print(random_numbers)
[1, 4, 3, 5, 3, 4, 5, 4, 5, 4]
>>> print(type(random_numbers))
<class 'list'>

Генератор выражений

>>> random_numbers = (random.randint(1,5) for x in range(10))
>>> print(random_numbers)
<generator object <genexpr> at 0x7fccb814e3c0>
>>> print(type(random_numbers))
<class 'generator'>

Как вы видите, при использовании list comprehension мы можем вывести полный список элементов list comprehension.

То же самое не относится к выражению генератора, которое просто возвращает объект генератора.

Чтобы получить следующий элемент из объекта-генератора, нам нужно использовать функцию Python next:

>>> print(next(random_numbers))
3
>>> print(next(random_numbers))
2

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

Цикл for против понимания списка: сравнение скорости

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

Создайте файл Python с именем for_loop_vs_list_comprehension.py со следующим кодом:

def get_numbers_using_for_loop():
    numbers = []

    for x in range(10):
        numbers.append(2*x)

    return numbers


def get_numbers_using_list_comprehension():
    numbers = [2*x for x in range(10)]
    return numbers

И убедитесь, что обе функции возвращают одинаковый результат:

print(get_numbers_using_for_loop())
print(get_numbers_using_list_comprehension())

[output]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Затем используйте модуль timeit для измерения скорости обеих функций:

$ python -m timeit -s "from for_loop_vs_list_comprehension import get_numbers_using_for_loop" "get_numbers_using_for_loop()"
500000 loops, best of 5: 868 nsec per loop

$ python -m timeit -s "from for_loop_vs_list_comprehension import get_numbers_using_list_comprehension" "get_numbers_using_list_comprehension()"
500000 loops, best of 5: 731 nsec per loop

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

Заключение

Мы узнали довольно много о list comprehension в Python!

Замечательно, как list comprehension может сделать ваш код намного более лаконичным и как оно может заменить несколько конструкций Python, основанных на циклах for, лямбда-выражениях и функциях map/reduce/filter.

Вы готовы начать использовать списковые генераторы прямо сейчас?

Если нет, прочтите эту статью еще раз и практикуйтесь, практикуйтесь, практикуйтесь 🙂

Автор

Фото аватара

Владимир Михайлов

Программист на Python с большим количеством опыта и разнообразных проектов.