Перейти к содержимому
Теория · Включения и генераторные выражения · урок 5 из 8

Генераторные выражения

В прошлых уроках конвейер сразу складывал все плитки в ящик. Но иногда ящик не нужен. Представьте, что пресс не работает впрок: лента сдвигается на одну плитку, только когда в конце её кто-то забирает. Такой конвейер выдаёт плитки по запросу и ничего не копит.

Зачем такой конвейер? Посмотрим на сумму.

Функция sum возвращает сумму элементов итерируемого объекта. Чтобы сложить квадраты чисел, в неё можно передать списковое включение. Но включение сначала строит весь список квадратов, хотя он нужен лишь на один проход. Без списка обходится генераторное выражение:

Python
nums = [3, 1, 4]
print(sum([n * n for n in nums]))  # 26
print(sum(n * n for n in nums))  # 26
squares = (n * n for n in nums)
print(list(squares))  # [9, 1, 16]
print(list(squares))  # []

Давайте разберём по строкам:

  1. sum([n * n for n in nums]) строит список [9, 1, 16] и складывает его — получается 26.
  2. sum(n * n for n in nums) получает те же квадраты по одному, без списка, и тоже печатает 26.
  3. squares = (n * n for n in nums) пока ничего не считает: эта строка только готовит конвейер.
  4. Первый list(squares) забирает все плитки и печатает [9, 1, 16].
  5. Второй list(squares) печатает []: лента уже пуста.

Генераторное выражение записывают как списковое включение, но в круглых скобках. Оно не строит список, а выдаёт значения по одному, когда их просят. Такое вычисление по запросу называют ленивым. Список из миллиона чисел занимает мегабайты памяти, а генераторное выражение хранит только текущий шаг перебора.

Скобки и печать

Если генераторное выражение — единственный аргумент вызова, вторые скобки не нужны: sum(n * n for n in nums). Внутри вызова с несколькими аргументами скобки пишут, как в строке squares = (n * n for n in nums).

print(squares) показывает не значения, а строку вида <generator object <genexpr> at 0x…>, как у enumerate. Значения собирает в список функция list.

Один проход

Генераторное выражение можно пройти только один раз. Когда все значения выданы, оно пустое, и второй list(squares) вернул [] без всякой ошибки. Здесь аналогия перестаёт работать: настоящую ленту можно загрузить снова, а пустое генераторное выражение не перезапустить. Для второго прохода напишите его заново или сохраните значения в список.

Этот вопрос часто задают на собеседовании: «Чем генераторное выражение отличается от спискового включения?» Ответ состоит из трёх частей:

  • включение сразу строит весь список, а генераторное выражение выдаёт значения по запросу;
  • генераторное выражение хранит только текущий шаг, сколько бы значений оно ни выдало;
  • его проходят один раз, и у него нет индексов и длины.

any и all

Функция any возвращает True, если истинен хотя бы один элемент, а all — если истинны все. С генераторным выражением они проверяют условие для каждого элемента:

Python
nums = [3, 1, 4]
print(any(n > 3 for n in nums))  # True
print(all(n > 3 for n in nums))  # False

Обе функции останавливаются, как только ответ ясен, — как or и and:

  • any прекращает перебор на первом истинном значении;
  • all прекращает перебор на первом ложном.

Остальные значения генераторное выражение уже не вычисляет. Для all здесь хватило первого числа: 3 > 3 ложно.

min и max с key

min и max принимают не только несколько аргументов, но и один итерируемый объект. С генераторным выражением max(len(w) for w in words) находит наибольшую длину — число 6, но не само слово. Слово вернёт именованный аргумент key: он задаёт, по какому значению сравнивать элементы.

Python
words = ["pear", "fig", "banana"]
print(max(len(w) for w in words))  # 6
print(min(words), max(words))  # banana pear
print(min(words, key=len), max(words, key=len))  # fig banana

Разберём три строки:

  1. max(len(w) for w in words) сравнивает длины и возвращает число 6.
  2. Без key строки сравниваются по алфавиту, буква за буквой, поэтому min(words) — это banana, а max(words) — pear.
  3. С key=len функции сравнивают длины, а возвращают сам элемент — слово, а не число.

Имя функции после key= пишут без скобок: её вызывают сами min и max, по разу для каждого элемента.

Частая ошибка

Круглые скобки приняли за кортеж и взяли элемент по индексу:

Python
nums = [3, 1, 4]
squares = (n * n for n in nums)
print(squares[0])  # ✗
TEXT
TypeError: 'generator' object is not subscriptable

Генераторное выражение не хранит значений, поэтому у него нет ни индексов, ни длины len. Нужен список — пишите включение в квадратных скобках или вызовите list(squares).

Главное

  • Генераторное выражение (n * n for n in nums) выдаёт значения по одному и не строит список.
  • Единственному аргументу вызова вторые скобки не нужны: sum(n * n for n in nums).
  • Генераторное выражение проходят один раз, и у него нет индексов и len.
  • min(words, key=len) сравнивает длины, а возвращает само слово.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом