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

Для создания списков есть очень мощный выразительный синтаксис 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.122855465110149, 0.122855465110149, 0.122855465110149]
[0.2369856559662733, 0.7855893575232121, 0.45901615100179716]

Аналог функции 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):
    return not x % 2

array = [randint(0, 100) for _ in range(20)]
only_even = list(filter(is_even, array))

print(array)
print(only_even)
[98, 97, 3, 70, 49, 10, 27, 70, 85, 95, 65, 46, 49, 21, 74, 82, 23, 60, 81, 2]
[98, 70, 10, 70, 46, 74, 82, 60, 2]

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

only_even = [x for x in array if not x % 2]
print(only_even)
[0, 90, 34, 72, 94, 100, 68, 68, 60, 84]

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

only_even_squared = list(map(my_square, filter(is_even, array)))
print(only_even_squared)

only_even_squared = [x * x for x in array if not x % 2]
print(only_even_squared)
[0, 8100, 1156, 5184, 8836, 10000, 4624, 4624, 3600, 7056]
[0, 8100, 1156, 5184, 8836, 10000, 4624, 4624, 3600, 7056]

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

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

Аналогия с математической записью множеств#

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

В качестве примера рассмотрим определение кольца вычетов по модулю \(n\)

\[ \mathbb{Z}_n = \big\{x\in\{1, \ldots, n-1\} \mid \gcd(x, n) > 1\big\}. \]

Здесь \(\gcd\) — наибольший общий делитель. При \(n=42\) множество элементов кольца принимает вид

\[ \mathbb{Z}_{42} = \big\{x\in\{1, \ldots, 41\} \mid \gcd(x, 42) = 1\big\} = \{ 1, 5, 11, 13, 17, 19, 23, 25, 29, 31, 37, 41\}. \]

В этом примере множество \(\mathbb{Z}_{42}\) задаётся как множество элементов другого множества (множества \(\{1, \ldots, 41\}\)), которые удовлетворяют условию взаимной простоты с числом 9. Такая логическая конструкция легко выражается списковым включением.

from math import gcd

n = 42
Zn = [x  for x in range(n) if gcd(x, n) == 1]
print(Zn)
[1, 5, 11, 13, 17, 19, 23, 25, 29, 31, 37, 41]

Важная деталь, которую стоит подметить — в математике множество не может содержать дубликатов, а список в python может. В качестве демонстрации рассмотрим множество квадратов не превосходящих по модулю 2 целых чисел

\[ R = \Big\{x^2 \mid x\in\{-2, -1, 0, 1, 2\}\Big\} = \{0, 1, 4\}. \]
R = [x ** 2 for x in range(-2, 3)]
print(R)
[4, 1, 0, 1, 4]

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

R = {x ** 2 for x in range(-2, 3)}
print(R)
{0, 1, 4}

Warning

Контейнер set устроен очень похоже на словари. Это значит, что он может хранить в себе только хэшируемые объекты, а значит поиск по нему осуществляется очень быстро. Дополнительное преимущество этого контейнера, что оператора | и & работают как объединение и пересечение множеств.

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

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

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'}

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

from string import ascii_lowercase, ascii_uppercase

vowels = {"a", "e", "i", "o", "u", "y"} 
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'}
{key(x):value(x) for x in container if condition(x)}

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

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

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

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

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

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

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

Пример с матрицей#

from pprint import pprint

matrix = [[10 * j + i for i in range(10)] for j in range(10)]
pprint(matrix)
[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
 [20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
 [30, 31, 32, 33, 34, 35, 36, 37, 38, 39],
 [40, 41, 42, 43, 44, 45, 46, 47, 48, 49],
 [50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
 [60, 61, 62, 63, 64, 65, 66, 67, 68, 69],
 [70, 71, 72, 73, 74, 75, 76, 77, 78, 79],
 [80, 81, 82, 83, 84, 85, 86, 87, 88, 89],
 [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]]