Материалы по курсу «Основы программирования»

Лекция 1. Списковые включения

Списковые включения (list comprehensions, списковые выражения) — синтаксис, позволяющий преобразовывать данные, представленные в виде итераторов или итерируемых объектов.

Списковые включения есть в различных языках программирования, кроме Python’а это, например, Haskell, Erlang, C# (так называемый Linq), Ruby. Вариантом списковых включений можно считать оператор SELECT в SQL — языке запросов к базам данных.

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

Списковое включение для итератора

Базовый синтаксис построения итератора:

(‹выражение с переменной ‹ПЕРЕМ››  for ‹ПЕРЕМ› in ‹ИСТОЧНИК›)

Здесь ‹ПЕРЕМ› — имя переменной, ‹ИСТОЧНИК› — некоторый итерируемый объект (список, строка, range, итератор какой-нибудь и т.д.), ‹выражение …› — некоторое выражение.

Итератор будет запрашивать из источника очередное значение, класть его в переменную ‹ПЕРЕМ› и вычислять значение выражения. По сути это эквивалент вызова

map(lambda ‹ПЕРЕМ›: ‹выражение с переменной ‹ПЕРЕМ››, ‹ИСТОЧНИК›)

Примеры:

# Квадраты натуральных чисел:
squares = (x**2  for x  in [1, 2, 3, 4, 5, 6])

print(*squares)          # Напечатает 1, 4, 9, 16, 25, 36
                         # Вызов эквивалентен
                         # print(1, 4, 9, 16, 25, 36)

# Длины слов в строке:
lengths = (len(w)  for w  in 'Quick brown fox jumps over the dog'.split())

print(*lengths)          # Напечатает 5 5 3 5 4 3 3

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

Во втором примере метод .split() разобьёт строку по пробелам на слова — получится список слов

['Quick', 'brown', 'fox', 'jumps', 'over', 'the', dog']

Каждое из слов будет по очереди класться в переменную w, итератор будет возвращать значение len(w) (т.е. длину слова в переменной w) для каждого из слов строки.

Для того, чтобы напечатать все значения, которые формирует итератор, мы используем вызов print(*‹итератор›) — из итератора будут извлечены все значения и переданы как аргументы функции print().

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

В списковом включении можно использовать и условия для фильтрации значений. Синтаксис:

(‹выражение›  for ‹ПЕРЕМ›  in ‹ИСТОЧНИК›  if ‹условие›)

Здесь

Семантика. Вычисляются только те выражения, для которых логическое выражение после if оказалось истинным. Эта конструкция сочетает в себе и поведение функции filter, и функции map. Аналог:

map(lambda ‹ПЕРЕМ›: ‹выражение›,
    filter(lambda ‹ПЕРЕМ›: ‹условие›, ‹ИСТОЧНИК›))

Пример. Для данного списка чисел вычислим квадраты только чётных чисел.

>>> xs = [1, 2, 3, 4, 5, 6, 7, 8]
>>> squares = (x**2  for x in xs  if x % 2 == 0)
>>> squares
<generator object ...>
>>> list(squares)
[4, 16, 36, 64]

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

>>> squares = x**2  for x in xs  if x % 2 == 0
SyntaxError: invalid syntax

Однако, если списковое включение является аргументом функции, то скобки можно не писать:

>>> sum(x**2  for x in xs  if x % 2 == 0)
120

Списковые включения для контейнеров

Выше мы показали, как породить итератор (т.н. «генератор»), генерирующий набор значений. Но итератор — вещь одноразовая (прочитать можно один раз) и ленивая (значения порождаются в процессе чтения, потому он и «генератор»). Часто бывает нужно породить контейнер (список, словарь, множество) из некоторых значений.

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

>>> numbers = [1, 2, 3, 4, 5, 6]
>>> squares_iter = (x**2  for x in xs)
>>> squares = list(squares_iter)

Генератор, конечно, можно записать внутрь конструктора списка:

>>> numbers = [1, 2, 3, 4, 5, 6]
>>> squares = list( (x**2  for x in xs) )

Скобки, конечно, можно опустить:

>>> numbers = [1, 2, 3, 4, 5, 6]
>>> squares = list(x**2  for x in xs)

Но в Python для этой цели есть специальный компактный синтаксис:

>>> numbers = [1, 2, 3, 4, 5, 6]
>>> squares = [x**2  for x in xs]

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

Аналогично, можно породить и множество (контейнер без повторяющихся значений):

>>> numbers = [1, 2, 3, 4, 5, 6]
>>> squares = {x**2  for x in xs}
>>> squares
{64, 1, 4, 36, 9, 16, 49, 25}

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

Можно порождать и словари:

>>> phrase = 'The quick brown fox jumps over the lazy dog'
>>> words = phrase.split()
>>> word_lens = { w : len(w)  for w in words }
>>> word_lens
{'The': 3, 'quick': 5, 'brown': 5, 'fox': 3, …}
>>> word_lens['fox']
3
>>> word_lens['brown']
5

В отличие от множества, элемент словаря (пара «ключ-значение») описывается с использованием знака :.

Вложенные переборы в списковых включениях

Внутри списковых включений можно использовать несколько вложенных for/in:

(‹выражение›  for ‹ПЕРЕМ_1› in ‹ИСТОЧНИК_1›
              ...
              for ‹ПЕРЕМ_N› in ‹ИСТОЧНИК_N›)

(‹выражение›  for ‹ПЕРЕМ1_› in ‹ИСТОЧНИК_1›
              ...
              for ‹ПЕРЕМ_N› in ‹ИСТОЧНИК_N›
              if ‹логическое выражение›)

Здесь приведён пример генератора, порождающего итератор, однако, этот синтаксис можно использовать и для порождения контейнеров (списки — […], множества — {…}, словари — {…:…}).

Внутри источников ‹ИСТОЧНИК_i› переменные ‹ПЕРЕМ_i› использовать нельзя, т.е. генераторы друг от друга не зависят. В ‹выражении› и ‹логическом выражении› переменные ‹ПЕРЕМ_i› использовать можно.

В общем случае, если в источниках по M_i значений, то в результирующем наборе будет M_1×M_2×…×M_N значений, т.е. количество значений перемножается. В математике это называется декартовым произведением. Декартово произведение сопоставляет для множеств A, B, C… с элементами a, b, c множество A×B×C×…, элементы которого являются кортежами (a, b, c, …).

Однако, если в списковом включении используется фильтрация (if ‹логическое выражение›), то результирующих значений будет меньше.

>>> [(x, y)  for x in range(5)
             for y in range(5)]
[(0, 0), (0, 1), (0, 2), …, (4, 3), (4, 4)]
>>> [x + y  for x in range(5)  for y in range(5)]
[0, 1, 2, 3, 4, 1, 2, 3, 4, 5, 2, 3, 4, 5, 6, 3, 4, 5, 6, 7, 4, 5, 6, 7, 8]
>>> [(x, y)  for x in range(5)
             for y in range(5)
             if x > y]
[(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2), (4, 0), (4, 1), (4, 2),
 (4, 3)]

В первых двух примерах у нас получилось по 5×5 = 25 значений, во втором только 10, т.к. мы построили пары только для чисел, где x больше y.

Меньше, чем M_1×…×M_N значений может быть и в случае словарей или множеств, т.к. во множествах не могут повторяться элементы, в словарях — ключи:

>>> {x + y  for x in range(5)
            for y in range(5)}
{0, 1, 2, 3, 4, 5, 6, 7, 8}
>>> {x + y : (x, y)  for x in range(5)
                     for y in range(5)}
{0: (0, 0), 1: (1, 0), 2: (2, 0), 3: (3, 0), 4: (4, 0), 5: (4, 1),
 6: (4, 2), 7: (4, 3), 8: (4, 4)}