Основы списковых включений

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

Создание списка копий и копирование списков

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

a_list = [1, 2, 3]
a_copy = [x for x in a_list]
print(a_copy)
[1, 2, 3]

Рассмотрим синтаксис:

[x for x in iterable]
  • [] квадратные скобочки означают, что содержимое будет списком;

  • x for x in iterable означает, что в список попадут элементы x, где x пробегает по iterable;

То, что попадёт в список необязательно должно зависеть от iterable. Например, можно создать список одинаковых элементов.

zeros = [0 for i in range(5)]
print(zeros)
[0, 0, 0, 0, 0]

Результат очень похож на выражение [0] * 5, но в данном случае выражение вычисляется заново на каждой итерации цикла. Т.е. можно действительно создать список разных списков.

many_empty_lists = [[] for i in range(5)]
many_empty_lists[0].append("Много разных списков")
print(many_empty_lists)
[['Много разных списков'], [], [], [], []]
one_empty_list = [[]] * 5 
one_empty_list[0].append("Один список")
print(one_empty_list)
[['Один список'], ['Один список'], ['Один список'], ['Один список'], ['Один список']]

Ещё это можно проиллюстрировать на примере с генерацией случайных чисел.

from random import random

print([random()] * 3) 
print([random() for _ in range(3)])
[0.7356432841560648, 0.7356432841560648, 0.7356432841560648]
[0.2206528569401075, 0.8297474878522821, 0.3362492619099857]

Аналог функции map

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

Note

Функция map делает вычисления лениво. Концепция ленивых вычислений будет обсуждаться при обсуждении генераторов и итераторов. Пока что надо знать, что, чтобы получить список, необходимо применить list к результату функции map.

import math

array = [1, 4, 9, 16]
array_1 = list(map(math.sqrt, array))
print(array_1)

def my_square(x):
    return x * x

array_2 = list(map(my_square, array_1))
print(array_2)
[1.0, 2.0, 3.0, 4.0]
[1.0, 4.0, 9.0, 16.0]

Можно имитировать функцию map с помощью списковых включений.

array_1 = [math.sqrt(x) for x in array]
array_2 = [my_square(x) for x in array_1]
print(array_1)
print(array_2)
[1.0, 2.0, 3.0, 4.0]
[1.0, 4.0, 9.0, 16.0]

При этом необязательно применять функцию. Допустимо любое выражение.

array_2 = [x * x for x in array_1]
print(array_2)
[1.0, 4.0, 9.0, 16.0]

Т.е. теперь синтаксис такой:

    [expression(x) for x in iterable]

Аналог функции filter

В python есть встроенная функция фильтрации filter, которая принимает на вход функцию f и итерируемый объект iterable. Она оставляет только те элементы iterable, на которых f возвращает True. Как и функция map, функция filter делает это лениво.

from random import randint

def is_even(x):
    # if x % 2:
    #     return False
    # else:
    #     return True
    return False if x % 2 else True # Тернарный оператор

array = [randint(0, 100) for _ in range(20)]
only_even = list(filter(is_even, array))
only_even_squared = list(map(my_square, filter(is_even, array)))
print(array)
print(only_even)
print(only_even_squared)
[14, 6, 67, 67, 37, 17, 41, 7, 8, 2, 78, 75, 90, 35, 74, 19, 63, 29, 92, 50]
[14, 6, 8, 2, 78, 90, 74, 92, 50]
[196, 36, 64, 4, 6084, 8100, 5476, 8464, 2500]

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

only_even = [x for x in array if x % 2 == 0]
only_even_squared = [x * x for x in array if x % 2 == 0]
print(only_even)
print(only_even_squared)
[14, 6, 8, 2, 78, 90, 74, 92, 50]
[196, 36, 64, 4, 6084, 8100, 5476, 8464, 2500]

Т.е. теперь синтаксис такой:

    [expression(x) for x in iterable if condition(x)]

Аналогия с множествами из математики

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

В качестве примера, рассмотрим множество заданной в виде

\[ R = \{x^2 | x\vdots 2, |x| < 10, x\in\mathbb{N}\} = \{1, 9, 25, 49, 81\}. \]

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

R = [x ** 2  for x in range(-9, 10) if x % 2]
print(R)
[81, 49, 25, 9, 1, 1, 9, 25, 49, 81]

Важная деталь, которую стоит подметить — в математике множество не может содержать дубликатов, а список в python может. Благо, есть встроенный тип данных set, который ведет себя куда более похоже на математическое множество и тоже поддерживает синтаксис включений:

R = {x ** 2 for x in range(-9, 10) if x % 2}
print(R)
{1, 9, 81, 49, 25}

“Словарные включения”

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

from string import ascii_lowercase

d = {symbol: symbol.upper() for symbol in ascii_lowercase}
print(d)
{'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F', 'g': 'G', 'h': 'H', 'i': 'I', 'j': 'J', 'k': 'K', 'l': 'L', 'm': 'M', 'n': 'N', 'o': 'O', 'p': 'P', 'q': 'Q', 'r': 'R', 's': 'S', 't': 'T', 'u': 'U', 'v': 'V', 'w': 'W', 'x': 'X', 'y': 'Y', 'z': 'Z'}

Используя функцию zip можно записать его иначе:

from string import ascii_lowercase, ascii_uppercase

d = {s:S for s, S in zip(ascii_lowercase, ascii_uppercase)}
print(d)
{'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D', 'e': 'E', 'f': 'F', 'g': 'G', 'h': 'H', 'i': 'I', 'j': 'J', 'k': 'K', 'l': 'L', 'm': 'M', 'n': 'N', 'o': 'O', 'p': 'P', 'q': 'Q', 'r': 'R', 's': 'S', 't': 'T', 'u': 'U', 'v': 'V', 'w': 'W', 'x': 'X', 'y': 'Y', 'z': 'Z'}

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

from string import ascii_lowercase, ascii_uppercase

vowels = ["a", "e", "i", "o", "u", "y"] 
d = d = {s:S for s, S in zip(ascii_lowercase, ascii_uppercase) if s not in vowels}
print(d)
{'b': 'B', 'c': 'C', 'd': 'D', 'f': 'F', 'g': 'G', 'h': 'H', 'j': 'J', 'k': 'K', 'l': 'L', 'm': 'M', 'n': 'N', 'p': 'P', 'q': 'Q', 'r': 'R', 's': 'S', 't': 'T', 'v': 'V', 'w': 'W', 'x': 'X', 'z': 'Z'}

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

print(set(ascii_lowercase) - set(vowels))
{'g', 'q', 'j', 'm', 'l', 'w', 'k', 'v', 'c', 'h', 'd', 't', 'p', 'f', 'b', 'z', 'r', 'x', 's', 'n'}

Больше возможностей

В одном списковом включении может быть несколько циклов.

array = [symbol * n for symbol in ["a", "b", "c"] for n in [1, 2, 3]]
print(array)
['a', 'aa', 'aaa', 'b', 'bb', 'bbb', 'c', 'cc', 'ccc']

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

array = [[symbol * n for symbol in ["a", "b", "c"]] for n in [1, 2, 3]]
print(array)
[['a', 'b', 'c'], ['aa', 'bb', 'cc'], ['aaa', 'bbb', 'ccc']]

Матрица

matrix = [[j * 4 + i for i in range(4)] for j in range(3)]
print(matrix)
[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]]