Словари и множества полезны лишь тогда, когда обладают полнотой информации об элементах: как иначе проверить наличие значения в множестве или ключа в словаре, если само множество или словарь окажутся не достроенными до конца?
Со списками ситуация иная: часто список как последовательность элементов нужно обойти ровно один раз. А иногда нам даже не требуется доходить до конца списка, если нашей задачей является поиск некоторого элемента.
Тут вы могли бы вспомнить курс про списки и концепцию итератора. А кто-то мог бы даже вспомнить тот факт, что map
и filter
не порождают списки сами по себе, а вместо этого порождают новые итераторы на основе итераторов-аргументов. В сочетании же с функциями из модуля itertools
конвейеры данных, работающие на основе итераторов получаются довольно эффективными, ведь никакая работа не выполняется, пока её результат не понадобится принимающей стороне на выходе конвейера!
Генераторы списков всем хороши, кроме того, что весь список будет создан так или иначе, даже если от списка будут не все элементы. Там, где выполнение цикла можно прервать с помощью break
, прервать вычисление генератора списков не получится. Кроме того, это будет выглядеть не декларативно.
Впрочем, вы не удивитесь умению Python использовать ленивость итераторов и в декларативном коде.
Когда выше утверждалось, что иногда последовательности не нужно вычислять целиком, это было небольшим лукавством: на самом деле получать и хранить законченные списки не нужно практически никогда! Да, в тех редких случаях, когда нужен именно список, пригодятся генераторы списков. Но большинство задач решается с помощью генераторных выражений (generator expressions). Выглядят оные как генераторы списков, только заключённые в круглые скобки вместо квадратных!
[x * x for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
(x * x for x in range(10))
# <generator object <genexpr> at 0x7fe76f7e5db0>
Как видите, результатом вычисления второго выражения является не список, а некий "generator object". Пусть название вас не смущает, важно лишь помнить, что этот объект является итератором — вот так мы и откладываем вычисление элементов последовательности до появления необходимости в них!
Заметьте, "generator object" является именно итератором, его не получится обойти несколько раз:
def print6(xs):
for i, x in enumerate(xs):
print(x)
if i == 5:
break
i = (x * x for x in range(10))
print6(i)
# => 0
# => 1
# => 4
# => 9
# => 16
# => 25
print6(i) # продолжаем перебирать элементы
# => 36
# => 49
# => 64
# => 81
print6(i) # больше ничего не осталось
Здесь __iter__
вызывается для итератора каждый раз, но итератор возвращает самого себя вместо нового итератора. Единожды вычисленные и использованные элементы нигде не сохраняются.
Часто можно встретить генераторное выражение в таком месте кода, где интерпретатор может однозначно понять, где границы этого выражения. Самый частый пример — генераторное выражение в роли единственного аргумента функции, то есть что-то такое: f((… for … in …))
. В подобных случаях скобки вокруг самого выражения можно опустить. Так же можно опускать скобки вокруг кортежа там, где это не мешает чтению кода.
Такое избавление от лишних скобок часто делает код ещё более лаконичным:
any(x > 100 for x in range(1000000))
# True
То есть буквально "(есть ли) любой икс больше ста среди иксов в диапазоне (от нуля) до миллиона"! И вычислится это выражение мгновенно, а числа будут проверяться по одному за раз. А вот если бы мы написали any([… for …])
, то Python всё так же искал бы первый True
в списке (и нашёл бы его быстро), но предварительно построил бы в памяти список в миллион элементов!
Ответ здесь будет максимально простой: везде, где можно использовать генераторные выражения, используйте их! Практически любые функции, которые работают с последовательностями в том или ином виде, смогут использовать generator objects. Даже при вызове функции для "пачки аргументов" лучше использовать генераторное выражение:
print(*(x for x in "Hello World!" if x.isupper()))
# => H W
И уж тем более стоит использовать генераторные выражения посреди list/set/dict comprehensions, генераторных выражений, конвейеров на основе map
/filter
.
Когда-то давным-давно в Python существовали только генераторы списков и генераторные выражения. В те времена множества и словари строили так:
set(x * x for x in range(10))
# {0, 1, 64, 4, 36, 9, 16, 49, 81, 25}
dict((x, x * x) for x in range(10))
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
Здесь генераторное выражение создаёт элементы по одному за раз. set()
и dict()
потребляют элементы итератора так же по одному, вставляя оные в нужные места. Это уже достаточно эффективный способ, отдельные синтаксические конструкции для set comprehensions и dict comprehensions просто повысили выразительность.
Вам ответят команда поддержки Хекслета или другие студенты.
Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.
Загляните в раздел «Обсуждение»:
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.
Наши выпускники работают в компаниях:
С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.
Зарегистрируйтесь или войдите в свой аккаунт