Lazy Evaluation
Lazy Evaluation — это ленивые вычисления или отложенные вычисления, которые применяются в некоторых языках программирования. Это стратегия вычисления, согласно которой вычисления следует откладывать до тех пор, пока не понадобится их результат.
В реальности это касается не только языков. Это применяется и на уровне библиотек. Сейчас мы увидим, зачем это нужно, и как это работает.
Ленивость в том или ином виде существует во всех языках программирования. В основном это касается логических выражений. И в Python она тоже есть.
Например, если мы встречаем такое логическое выражение, то его выполнение идет слева направо:
# True
True or print('message')
Если мы проверяем True
, и далее стоит оператор "или" (or
), то нам неважно, что будет справа. Эта часть кода не повлияет на то, что результатом будет истина.
Это такая стратегия оптимизации внутри, которая позволяет не вычислять правое значение. Программисты пользуются этим, чтобы проверять существование какого-то объекта, например, что он не равен None
, и вызывать дальше какой-то метод. Это проверка на существование позволяет не писать сложные куски кода.
Работа с коллекциями
Основная область применения ленивых вычислений - работа с коллекциями. В Python есть встроенная функция reversed()
:
nums = [1, 2, 3, 4, 5]
reversed(nums) # <list_reverseiterator at 0x75e0455d73d0>
Вместо списка функция вернула объект итератор. Итераторы это особые объекты, представляющие собой поток данных. С понятием потока данных (или стримами "stream") вы будете также встречаться в других языках, и везде они используются для ленивых вычислений. Потоки особенно важны при работе с большими объемами данных или при обработке данных в режиме реального (потокового) времени. Потоки позволяют обрабатывать элементы по запросу, а не загружать все данные сразу в память.
Получать элементы из итератора можно функцией next()
, либо самое простое, обойти их в цикле. Иначе говоря, итерироваться.
nums = [1, 2, 3, 4, 5]
for elem in reversed(nums):
print(elem)
# => 5
# => 4
# => 3
# => 2
# => 1
В действительности, при использовании цикла for .. in
у коллекции запрашивается метод __iter__()
, возвращающий итератор. А на каждом шаге итерации вызывается метод __next__()
, возвращающий следующий элемент коллекции. Также Python позволяет получать объект итератора явно, используя функцию iter()
.
В примере выше reversed()
не возвращает сразу новую перевернутую коллекцию, а создает итератор, который уже по запросу отдает по одному элементу с конца коллекции. Так можно в целом описать концепцию ленивых вычислений - не вычислять ничего, пока не нужно. Таким образом мы экономим вычислительные ресурсы, можем обрабатывать очень большие объемы данных (и даже "бесконечные"), а еще выстраивать цепочку вычислений и запускать ее по требованию.
При работе с итераторами важно помнить, что по итератору можно двигаться лишь в одном направлении пока итератор не исчерпает себя. Также если мы прервем обработку и вернемся к итератору вновь, то обработка продолжится с места остановки.
numbers = [1, 2, 3, 4, 5]
# получим итератор явно
it = iter(numbers)
for elem in it:
print(elem)
# прервем выполнение
if elem == 3:
break
# => 1
# => 2
# => 3
# и вернемся позже
for elem in it:
print(elem)
# => 4
# => 5
Генераторы
Помимо встроенных функций как reversed()
, возвращающих итераторы, в Python есть инструменты для создания своих ленивых функций.
def gen_squares_to(n):
i = 1
while i <= n:
yield i ** 2
i += 1
for n in gen_squares_to(5):
print(n)
# => 1
# => 4
# => 9
# => 16
# => 25
Подобные функции, которые могут вести себя как итераторы и на каждом шаге итерации генерируют новое значение, называют генераторами.
Определение функции выше похоже на привычное нам за исключением нового слова yield
, которое используется вместо return
. Основное отличие генераторных функций, что после исполнения yield
, функция не завершается, а приостанавливается до нового шага итерации. Это позволяет исполнять код после выражения yield
, использовать несколько yield
и даже реализовывать бесконечные потоки данных:
def gen_squares():
i = 1
# бесконечный цикл
while True:
yield i ** 2
i += 1
result = []
for num in gen_squares():
result.append(num)
if num > 100:
break
print(result) # => [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121]
Выводы
В этом уроке мы узнали, что такое Lazy Evaluation. Это стратегия вычисления, согласно которой вычисления следует откладывать до тех пор, пока не понадобится их результат.
Также мы узнали, как ленивые вычисления помогают при работе с коллекциями. Узнали почему функции для обработки коллекций возвращают итераторы, потоки данных. Научились создавать собственные функции генерирующие потоки данных. Ленивые вычисления и потоковая обработка это ключевые инструменты в работе с большими данными.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.