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

Курс по Python: https://stepik.org/course/100707

Мы продолжаем тему генераторов списков языка Python. На прошлом занятии мы увидели, как можно формировать списки, используя конструкции:

[<способ формирования значения> for <счетчик> in <итерируемый объект>]

и

[<способ формирования значения>
   for <счетчик> in <итерируемый объект>
   if <условие>
]

Но, в действительности, Python позволяет записывать любое число циклов for в генераторах списков. Здесь операторы if после циклов являются необязательными (мы их можем прописывать, а можем и пропускать).

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

a = [(i, j) for i in range(3) for j in range(4)]
print(a)

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

a = [(i, j)
     for i in range(3)
     for j in range(4)
]

Вот отсюда уже гораздо лучше видно, что второй цикл вложен в первый. То есть, сначала при i = 0 отрабатывает внутренний цикл по j от 0 до 3. Затем, переходим к первому циклу, i увеличивается на 1 и внутренний цикл повторяется. В результате, получаем пары чисел:

[(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3)]

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

a = [(i, j)
     for i in range(3) if i % 3 == 0
     for j in range(4)
]

Или, у обоих вместе:

a = [(i, j)
     for i in range(3) if i % 3 == 0
     for j in range(4) if j % 2 != 0
]

Используя этот подход, мы можем, например, сформировать таблицу умножения:

a = [f"{i}*{j} = {i*j}"
     for i in range(1, 4)
     for j in range(1, 4)
]

Или, двумерный список превратить в одномерный:

matrix = [[0, 1, 2, 3],
          [10, 11, 12, 13],
          [20, 21, 22, 23]]
 
a = [x
     for row in matrix
     for x in row
]

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

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

Давайте еще раз посмотрим на исходное определение генератора списка:

[<оператор> for <счетчик> in <итерируемый объект>]

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

[[генератор списка]
   for <переменная> in <итерируемый объект>
]

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

M, N = 3, 4
matrix = [[a for a in range(M)] for b in range(N)]
 
print(matrix)

Получаем двумерный список, вида:

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

Результат вполне очевиден. Смотрите, сначала отрабатывает первый (внешний) генератор списка и переменная b = 0. Затем, выполнение переходит к вложенному генератору, который выдает список [0, 1, 2]. Этот список помещается как первый элемент основного списка. Далее, снова отрабатывает первый генератор и b принимает значение 1. После этого переходим к вложенному генератору, который возвращает такой же список [0, 1, 2]. И так пока не закончится работа первого генератора. В итоге, видим список, в который вложены четыре других списка.

Где может пригодиться такой подход? Например, для изменения значений двумерного списка. Давайте предположим, что у нас есть вот такой список:

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

И мы хотим возвести все его значения в квадрат. Лучше всего это сделать именно через генератор, следующим образом:

A = [[x ** 2 for x in row] for row in A]

В первом генераторе происходит перебор строк (вложенных списков) матрицы A, а во вложенном генераторе – обход элементов строк матрицы. Каждое значение возводится в квадрат и на основе этого формируется текущая строка. Обратите внимание, что во вложении мы можем использовать переменные из внешнего генератора списка, в частности, переменную row, ссылающуюся на текущую строку матрицы A.

Другой пример – это транспонирование матрицы A (то есть, замена строк на столбцы) с использованием вложенных генераторов. Сделать это можно, следующим образом:

A = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
A = [[row[i] for row in A] for i in range(len(A[0]))]

Поясню работу этой конструкции. Сначала значение i = 0, а переменная row[i] пробегает первые значения строк матрицы A. В результате формируется первая строка транспонированной матрицы. Далее, i увеличивается на 1 и row[i] пробегает уже вторые элементы строк матрицы A. Получаем вторую строку транспонированной матрицы. И так делаем для всех столбцов. На выходе формируется транспонированная матрица.

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

g = [u ** 2 for u in [x+1 for x in range(5)]]

Здесь сначала отрабатывает вложенный генератор списка, получаем список [1, 2, 3, 4, 5], а затем, эти значения перебираются первым генератором и возводятся в квадрат, получаем результат:

[1, 4, 9, 16, 25]

Это очень похоже на вычисление значений сложной (вложенной) функции:

g(u(x+1)) = (x+1) ^ 2

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

Курс по Python: https://stepik.org/course/100707

Видео по теме