Выражения-генераторы списков, словарей, множеств, итераторов

Представьте, что нам нужно создать список из чисел от 1 до 100, не кратных 3 (те, которые при делении на 3 дают ненулевой остаток).

Можно создать такой список через цикл for:

numbers = []
for number in range(1, 101):
    if number % 3:
        numbers.append(number)

Получился код на 4 строки. При этом мы создаём пустой список, а потом каждый раз его заполняем по одному элементу.

Генераторы списков

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

numbers = [number for number in range(1, 101) if number % 3]

Как соотносятся цикл и генератор?

  1. numbers = [] -> [...] - В генераторе список создаётся сразу внутри квадратных скобок
  2. for number in range(1, 101): -> for number in range(1, 101) - Цикл for остаётся таким же (разве что не нужно двоеточие, поскольку в генераторе нет вложенных блоков с необходимостью дополнительного отступа)
  3. if number % 3: -> if number % 3 - Условие (или условия, их может быть несколько как в цикле, так и в генераторе) остаётся таким же
  4. numbers.append(number) -> number - В генераторе списка элемент добавляется напрямую, без использования метода append

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

>>> multiplication_table = [[i * j for j in range(1, 5)] for i in range(1, 5)]
>>> print(multiplication_table)
[[1, 2, 3, 4], [2, 4, 6, 8], [3, 6, 9, 12], [4, 8, 12, 16]]

Вложенные генераторы

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

Например, создадим список пар чисел, где первое число от 1 до 3, а второе от 1 до 2:

pairs = [(x, y) for x in range(1, 4) for y in range(1, 3)]

Это эквивалентно следующим циклам:

pairs = []
for x in range(1, 4):
    for y in range(1, 3):
        pairs.append((x, y))

Вложенные генераторы удобно использовать, если необходима фильтрацию на каждом уровне цикла. Например, создадим список пар чисел, где первое число - нечётное от 1 до 5, а второе от 1 до 2, при этом исключая те пары, где сумма чисел равна 5:

pairs = [(x, y) for x in range(1, 6) if x % 2 for y in range(1, 3) if x + y != 5]

Это эквивалентно следующему циклу:

pairs = []
for x in range(1, 6):
    if x % 2:
        for y in range(1, 3):
            if x + y != 5:
                pairs.append((x, y))

Генераторы множеств

Подобным же образом можно создавать множества, только вместо квадратных скобок - фигурные:

numbers_unique = {number for number in range(1, 101) if number % 3}

Генераторы словарей

С помощью выражения-генератора можно создавать и словари (здесь в качестве примера мы возьмём значения для ключа, равное квадрату числа):

numbers = {number: number ** 2 for number in range(1, 101) if number % 3}

Генераторы итерируемых объектов

А что же будет, если скобки круглые? Нет, не генератор кортежей. Это будет генератор лениво итерируемого объекта:

>>> numbers = (number for number in range(1, 11) if number % 3)
>>> print(numbers)
<generator object <genexpr> at 0x0000016351173780>
>>> for number in numbers:
...     print(number)
...
1
2
4
5
7
8
10
  • Занимает меньше памяти
  • Выполняется ровно один раз, после чего во второй раз в цикле for он будет пустым
  • Каждый элемент вычисляется только в момент, когда он понадобится. Все ошибки при генерации элементов будут встречены лишь внутри цикла, в котором этот итератор будет использован

С примером такого итератора мы уже встречались, это объект range.

Когда уместны генераторы

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

  • Сложные логические условия и вычисления: Если логика создания элементов сложна и занимает много строк, использование генераторов сделает код трудным для понимания
  • Многоуровневые вложенные циклы: При глубокой вложенности циклов генераторы могут ухудшить читаемость
  • Отладка: Генераторы могут быть сложнее для отладки, особенно если в процессе создания коллекции возникают ошибки

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