Кстати, меня с недавних пор начал мучить вопрос как правильно «бложик» или «бложек»? Вы как думаете?
А теперь, генераторы.
Шучу.
1. Что такое генератор?
Напомню, что генератором-функцией является та функция, которая содержит в себе инструкцию yield. При вызове метода next объекта этой функции мы получим значение, указанное за yield, и весь контекст, включая значение локальных переменных, будет запомнен в том состоянии, в котором он находятся при выполнении строчки с yield. Например.
def gener(stop):
i = 0
while i < stop:
yield i
i += 1
Так, создав объект
g = gener(7)
и вызвав метод next
g.next()
получим сначала
0
После чего параметры функции i = 0 и то, что выполнение функции находится внутри тела цикла, будет запомнено до следующего вызова next. Следующий вызов next, вернет 1.
g.next()
1
Так будет происходить, пока условие i < stop выполняется, а именно до тех пор, пока i не достигнет значения 7.
...
g.next()
6
g.next()
StopIteration
Таким образом, при достижении «конца» итераций, возвращается исключение StopIteration. Выход из генератора можно вызвать явно, написав где-либо в функции инструкцию return, при достижении которой опять же вернется исключение StopIteration.
То есть объявление типа
def gener(stop):
i = 0
while i < stop:
yield i
i += 1
return
равнозначно предыдущей функции gener.
Часть 2. Зачем нужны такие на первый взгляд бесполезные функции, возвращающие генераторы?
Известно, что объекты типа питоновских списков занимают память пропорционально количеству элементов. Скажем список [0, 1, 2, 3, 4, 5, 6] будет затрачивать память как минимум под шесть целочисленных значений. Если нам нужно будет итерироваться по такому списку, то мы можем явно написать что-то вроде
for i in [0, 1, 2, 3, 4, 5, 6]
print i
и получить на стандартный вывод последовательность от 0 до 6.
Если же нам понадобится итерироваться по списку с гораздо большим количеством целых чисел, скажем, от нуля до нескольких миллиардов с шагом 1, то неужели придется хранить все эти числа в памяти? Это не нужно. Достаточно знать начало, конец и шаг итераций. Для этого в Питоне 2.x используют функцию xrange, она возвращает генератор, который в каждый момент времени хранит только начальное, конечное значения, шаг, и текущее значение, что намного меньше, чем несколько миллиардов чисел. Такую функцию можем и написать мы сами, используя yield. Возможно, правда, xrange будет работать чуть эффективнее, потому что разработчики постарались как-нибудь ее оптимизировать.
И это еще не всё. Что, если мы захотим хранить объект из бесконечного числа субобъектов? Мы можем оперировать этим понятием в жизни, но не в программировании? Не может быть! Конечно не может. Чтобы убедиться в этом, достаточно допустить ошибку в предыдущей функции.
def gener():
i = 0
while True:
yield i
i += 1
Вы видите описанный бесконечный цикл. Итерироваться по объекту, возвращенному такой функцией, можно бесконечно. Сам объект будет символом бесконечного множества.
g = gener()
И все дела.
Часть 3. Загадочный метод send.
У объектов-генераторов еще есть метод send, который на первый взгляд тоже кажется довольно бесполезным ;)
Если вызвать метод send у генератора, тогда yield внутри функции-генератора вернет значение, отправленное методом send. По умолчанию yield возвращает значение None.
Напишем такую функцию-генератор. Если yield возвращает None, то к i прибавляется единица, если же нет, то i становится равным x. Так можно в генератор посылать новое значение, с которого начнет итерироваться i снова.
def gener():
i = yield
while True:
x = yield i
if not x:
i += 1
else:
i = x
g = gener()
g.next()
0
g.send(2)
2
g.next()
3
И т. д. С помощью send в данном случае можно определять значение, с которого нужно итерироваться, создав только один объект-генератор.
Вообще мне тут сказали, что генератор, в который можно посылать начальное значение не очень имеет смысл, так как вместо этого можно создать просто еще один объект-генератор с фиксированным начальным значением, но все же я решила, что это неплохой пример действия метода send.
Часть 4. Как использовать объекты с бесконечной генерацией чисел?
Во-первых, есть такая библиотека itertools, в которой много полезных функций для оперирования такого рода объектами.
В пример приведу одну из них islice. Функция islice принимает как минимум объект-генератор и значение, до которого нужно итерироваться. Возвращает конечный итератор.
from itertools import islice
def gener():
i = 0
while True:
yield i
i += 1
for i in islice(gener(), 5)
print i,
0 1 2 3 4
Можно написать аналог такой функции, используя генератор.
def gener(*args):
if args == []:
step = 1
else:
step = args[0]
i = yield
while True:
x = yield i
if not x:
i += step
else:
i = x
def slice(gen, start, stop, step):
g = gen(step)
g.send(None)
x = g.send(start)
yield x
while True:
x = g.next()
if x < stop:
yield x
else:
raise StopIteration
for i in slice(gener, 0, 10, 2):
print i,
0 2 4 6 8
На этом всё. Кажется последние два пункта получились хуже, чем первые два, это потому что первые два я писала до обеда, а вторые — после. Ну, ничего. Лучше, ищите ошибки.
http://simeonvisser.com/posts/python-3-using-yield-from-in-generators-part-1.html
ОтветитьУдалитьПривет от Python 3.3 =))
.next() уже не работает, теперь нужно писать __next__()
ОтветитьУдалить