В Python часто используются generator
иyield
. Расскажу в этой статье об основных свойствах generator
, а также преимуществах работы с ним. Разберёмся в подробностях, как пользоваться yield
, чтобы создавать generator
.
А ещё изучим две другие концепции из информатики: ленивые (отложенные) вычисления и потоки данных (стримы).
Итерируемые объекты
Для начала узнаем, что такое итерируемый объект, а затем разберёмся, как используется generator
— в сущности это тоже итератор.
В Python итерируемый объект — это объект, над которым производятся так называемые проходы (итерации). Например, как в цикле for
.
Большинство наборных структур данных являются итерируемыми объектами. Это списки, кортежи, наборы. Например, ниже мы создаём список и проходимся по его элементам по очереди.
lst = [1, 2, 3]
for i in lst:
print(i)
# 1
# 2
# 3
lst = [x+x for x in range(3)]
for x in lst:
print(x)
# 0
# 2
# 4
Так же мы можем проитерировать и символы в строке.
string = "cat"
for c in string:
print(c)
# c
# a
# t
Ограничение итераций
Ограничивать количество итераций нужно для того, чтобы хранить все значения в памяти до их итерирования. Это будет занимать слишком много памяти в некоторых сценариях. Типичная ситуация — чтение строчек из файла.
def file_reader(file_path):
fp = open(file_path)
return fp.read().split("\n")
Представьте себе, что случится, если мы будем читать большой файл размером в 6 Гб. Нам нужно сохранить все строчки в памяти в процессе выгрузки содержимого из файла.
В реальности же нам часто нужно только проитерировать строчки по очереди, чтобы завершить определённые задачи по обработке данных. Нет необходимости загружать все строчки в память — можем прервать цикл заблаговременно.
Можно ли продумать стратегию на случаи, когда надо по необходимости прочитать данные? Да, для решения этой проблемы в Python есть generator
.
Генератор
generator
— тоже итератор, но его ключевое свойство — ленивые вычисления. Это классическая концепция в информатике, и её переняли многие языки программирования, такие как Haskell. Основная идея этой концепции звучит как вызов-по-необходимости. Отложенные вычисления могут приводить к снижению доступной процессу памяти.
Генератор — это итератор, который работает в режиме обработки по необходимости. Мы не будем производить вычисления и сохранять значения сразу, а сделаем их “на лету”, когда будут выполняться итерации.
Доступно два способа создания generator
: выражение генератора и функция генератора.
Выражение-генератор похож на преобразование списка, за исключением детали ()
. Раз generator
является итератором, мы пользуемся функцией next
, чтобы получить следующий элемент.
g1 = (x*x for x in range(10))
print(type(g1))
print(next(g1))
print(next(g1))# <type 'generator'>
# 0
# 1
Разница тут в том, что мы не вычисляем все значения при создании generator
. x*x
вычисляется тогда, когда мы итерируем generator
.
Чтобы понять разницу, давайте запустим сниппет кода.
>>> import timeit
>>> timeit.timeit('lst = [time.sleep(1) for x in range(5)]', number=2)
10.032547950744629
>>> timeit.timeit('lst = (time.sleep(1) for x in range(5))', number=2)
1.0013580322265625e-05
Как можем видеть из результата, когда мы создаём итерируемый объект, вычисление занимает 10 секунд, потому что мы извлекаем time.sleep(1)
10 раз.
Но в реальности, когда мы создаём generator
, time.sleep(1)
не выполняется.
Yield
Другой способ создать generator
— использовать функцию генератора. Мы берём ключевое слово yield
, чтобы вернуть generator
в функции.
Давайте посмотрим, как сработает эта функция на fib
, где возвращается generator
с n числами Фибоначчи.
def fib(cnt):
n, a, b = 0, 0, 1
while n < cnt:
yield a
a, b = b, a + b
n = n + 1
g = fib(10)
for i in range(10):
print g.next(),
# 0 1 1 2 3 5 8 13 21 34
Давайте применим yield
, чтобы переписать программу чтения файла, приведённую выше.
def file_reader(file_path):
for row in open(file_path, "r"):
yield row
for row in file_reader('./demo.txt'):
print(row),
С таким подходом мы не будем загружать всё содержимое в память. Вместо этого мы загрузим его путём чтения строк.
Поток данных
С генератором мы создадим структуру данных с бесконечным количеством элементов. Этот вид последовательности элементов данных называется в информатике потоком данных (или “стрим”). С его помощью мы можем выражать концепции бесконечных последовательностей математическими методами.
Например, нам нужна последовательность со всеми числами Фибоначчи. Как мы её получим?
Нам всего-то нужно убрать параметр счётчика из функции выше.
def all_fib():
n, a, b = 0, 0, 1
while True:
yield a
a, b = b, a + b
n = n + 1all_fib_numbers = all_fib()
Вуаля! Мы получаем переменную, которая могла бы отражать все числа Фибоначчи. Давайте напишем общую функцию, чтобы взять n элементов из любого потока.
def take(n, seq):
result = []
try:
for i in range(n):
result.append(next(seq))
except StopIteration:
pass
return result
Выражение take(all_fib_numbers, 10)
будет в результате возвращать первые 10 чисел Фибоначчи.
Заключение
generator
в языке Python — это мощный инструмент для отложенных вычислений, экономии памяти и времени.
Ключевая идея отложенных вычислений — рассчитать значение до того, как оно вам действительно понадобится. Это также помогает нам выражать концепции бесконечных последовательностей.
Читайте также:
- Потоковые и многопроцессорные модули на Python
- Автоматизация скриптов на Python при помощи AWS Lightsail
- На собеседование в Google через челлендж Python #1
Перевод статьи Coder’s Cat: What Are Generators, Yields, and Streams in Python?