Генераторные выражения
В прошлых уроках конвейер сразу складывал все плитки в ящик. Но иногда ящик не нужен. Представьте, что пресс не работает впрок: лента сдвигается на одну плитку, только когда в конце её кто-то забирает. Такой конвейер выдаёт плитки по запросу и ничего не копит.
Зачем такой конвейер? Посмотрим на сумму.
Функция sum возвращает сумму элементов итерируемого объекта. Чтобы сложить квадраты чисел, в неё можно передать списковое включение. Но включение сначала строит весь список квадратов, хотя он нужен лишь на один проход. Без списка обходится генераторное выражение:
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)) # []Давайте разберём по строкам:
sum([n * n for n in nums])строит список[9, 1, 16]и складывает его — получается26.sum(n * n for n in nums)получает те же квадраты по одному, без списка, и тоже печатает26.squares = (n * n for n in nums)пока ничего не считает: эта строка только готовит конвейер.- Первый
list(squares)забирает все плитки и печатает[9, 1, 16]. - Второй
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 — если истинны все. С генераторным выражением они проверяют условие для каждого элемента:
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: он задаёт, по какому значению сравнивать элементы.
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Разберём три строки:
max(len(w) for w in words)сравнивает длины и возвращает число6.- Без
keyстроки сравниваются по алфавиту, буква за буквой, поэтомуmin(words)— этоbanana, аmax(words)—pear. - С
key=lenфункции сравнивают длины, а возвращают сам элемент — слово, а не число.
Имя функции после key= пишут без скобок: её вызывают сами min и max, по разу для каждого элемента.
Частая ошибка
Круглые скобки приняли за кортеж и взяли элемент по индексу:
nums = [3, 1, 4]
squares = (n * n for n in nums)
print(squares[0]) # ✗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 и подпишись на канал — откроются все уроки.