Зарегистрируйтесь, чтобы продолжить обучение

Ленивые вычисления Python: Функции

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") вы будете также встречаться в других языках, и везде они используются для ленивых вычислений. Потоки особенно важны при работе с большими объемами данных или при обработке данных в режиме реального (потокового) времени. Потоки часто используются при чтении и записи файлов, при работе с видео- и аудиоданными, а также для работы со стримингами. Потоки позволяют обрабатывать элементы по запросу, а не загружать все данные сразу в память, что очень важно, так как данные могут быть потенциально бесконечными.

В примере выше reversed() не возвращает сразу новую перевернутую коллекцию, а создает итератор, который уже по запросу отдает по одному элементу с конца коллекции. Так можно в целом описать концепцию ленивых вычислений - не вычислять ничего, пока не нужно. Таким образом мы экономим вычислительные ресурсы, можем обрабатывать очень большие объемы данных (и даже "бесконечные"), а еще выстраивать цепочку вычислений и запускать ее по требованию.

Получать элементы из итератора можно несколькими способами.

  • Функцией next(), которая вызывает у итератора метод __next__(), а уже он извлекает из итератора следующий элемент. Если попытаться вызвать next() после последнего элемента, то функция сгенерирует исключение StopIteration:
nums = [1, 2, 3, 4, 5]
reversed_nums = reversed(nums)
next(reversed_nums) # 5
next(reversed_nums) # 4
next(reversed_nums) # 3
next(reversed_nums) # 2
next(reversed_nums) # 1
next(reversed_nums) # StopIteration
  • Итератор можно обойти в цикле. На самом деле, обход не только итератора, но и любой коллекции в цикле for .. in выглядит так: у коллекции запрашивается метод __iter__(), возвращающий итератор. Затем, на каждом шаге итерации вызывается метод __next__(). А когда сгенерируется исключение StopIteration, Python прекратит обход. То есть алгоритм в точности повторяет, то что мы видели выше, но автоматизирован.
nums = [1, 2, 3, 4, 5]

for elem in reversed(nums):
    print(elem)

# => 5
# => 4
# => 3
# => 2
# => 1
  • Также итератор можно конвертировать в составной тип данных типа списка или словаря, вызвав соответсвующую функцию list или dict. В таком случае, Python также обойдет весь итератор до конца, и создаст из него новую коллекцию.
nums = [1, 2, 3, 4, 5]
reversed_nums = reversed(nums)
list(reversed_nums) # [5, 4, 3, 2, 1]

Также Python позволяет получать объект итератора из коллекции явно, используя функцию iter():

l = [1, 2, 3]
iter(l) # <list_iterator at 0x76a447ebb7f0>

При работе с итераторами важно помнить, что по итератору можно двигаться лишь в одном направлении пока итератор не исчерпает себя. Также если мы прервем обработку и вернемся к итератору вновь, то обработка продолжится с места остановки.

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

# мы обошли весь итератор, и теперь он пустой
list(it) # []

Генераторы

Помимо встроенных функций как 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 и возобновит свое выполнение.

С помощью генераторов можно реализовывать собственные потоковые данные или даже реализовывать бесконечные потоки данных:

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. Это стратегия вычисления, согласно которой вычисления следует откладывать до тех пор, пока не понадобится их результат.

Также мы узнали, как в Python реализуются ленивые вычисления с помощью итераторов и генераторов. Узнали, что функции для обработки коллекций часто возвращают итераторы, потоки данных, для эффективного ипользования памяти. Также мы научились использовать итераторы и создавать собственные функции, генерирующие потоки данных. Ленивые вычисления и потоковая обработка - это ключевые инструменты в работе с большими данными.


Дополнительные материалы

  1. Lazy evaluation

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»